##// END OF EJS Templates
IOStream: Ignore missing attrs from `dir()`. #6386...
Christopher Welborn -
Show More
@@ -1,240 +1,245 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 IO related utilities.
3 IO related utilities.
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9
9
10
10
11 import atexit
11 import atexit
12 import os
12 import os
13 import sys
13 import sys
14 import tempfile
14 import tempfile
15 import warnings
15 import warnings
16 from warnings import warn
16 from warnings import warn
17
17
18 from IPython.utils.decorators import undoc
18 from IPython.utils.decorators import undoc
19 from .capture import CapturedIO, capture_output
19 from .capture import CapturedIO, capture_output
20 from .py3compat import input
20 from .py3compat import input
21
21
22 @undoc
22 @undoc
23 class IOStream:
23 class IOStream:
24
24
25 def __init__(self, stream, fallback=None):
25 def __init__(self, stream, fallback=None):
26 warn('IOStream is deprecated since IPython 5.0, use sys.{stdin,stdout,stderr} instead',
26 warn('IOStream is deprecated since IPython 5.0, use sys.{stdin,stdout,stderr} instead',
27 DeprecationWarning, stacklevel=2)
27 DeprecationWarning, stacklevel=2)
28 if not hasattr(stream,'write') or not hasattr(stream,'flush'):
28 if not hasattr(stream,'write') or not hasattr(stream,'flush'):
29 if fallback is not None:
29 if fallback is not None:
30 stream = fallback
30 stream = fallback
31 else:
31 else:
32 raise ValueError("fallback required, but not specified")
32 raise ValueError("fallback required, but not specified")
33 self.stream = stream
33 self.stream = stream
34 self._swrite = stream.write
34 self._swrite = stream.write
35
35
36 # clone all methods not overridden:
36 # clone all methods not overridden:
37 def clone(meth):
37 def clone(meth):
38 return not hasattr(self, meth) and not meth.startswith('_')
38 return not hasattr(self, meth) and not meth.startswith('_')
39 for meth in filter(clone, dir(stream)):
39 for meth in filter(clone, dir(stream)):
40 setattr(self, meth, getattr(stream, meth))
40 try:
41 val = getattr(stream, meth)
42 except AttributeError:
43 pass
44 else:
45 setattr(self, meth, val)
41
46
42 def __repr__(self):
47 def __repr__(self):
43 cls = self.__class__
48 cls = self.__class__
44 tpl = '{mod}.{cls}({args})'
49 tpl = '{mod}.{cls}({args})'
45 return tpl.format(mod=cls.__module__, cls=cls.__name__, args=self.stream)
50 return tpl.format(mod=cls.__module__, cls=cls.__name__, args=self.stream)
46
51
47 def write(self,data):
52 def write(self,data):
48 warn('IOStream is deprecated since IPython 5.0, use sys.{stdin,stdout,stderr} instead',
53 warn('IOStream is deprecated since IPython 5.0, use sys.{stdin,stdout,stderr} instead',
49 DeprecationWarning, stacklevel=2)
54 DeprecationWarning, stacklevel=2)
50 try:
55 try:
51 self._swrite(data)
56 self._swrite(data)
52 except:
57 except:
53 try:
58 try:
54 # print handles some unicode issues which may trip a plain
59 # print handles some unicode issues which may trip a plain
55 # write() call. Emulate write() by using an empty end
60 # write() call. Emulate write() by using an empty end
56 # argument.
61 # argument.
57 print(data, end='', file=self.stream)
62 print(data, end='', file=self.stream)
58 except:
63 except:
59 # if we get here, something is seriously broken.
64 # if we get here, something is seriously broken.
60 print('ERROR - failed to write data to stream:', self.stream,
65 print('ERROR - failed to write data to stream:', self.stream,
61 file=sys.stderr)
66 file=sys.stderr)
62
67
63 def writelines(self, lines):
68 def writelines(self, lines):
64 warn('IOStream is deprecated since IPython 5.0, use sys.{stdin,stdout,stderr} instead',
69 warn('IOStream is deprecated since IPython 5.0, use sys.{stdin,stdout,stderr} instead',
65 DeprecationWarning, stacklevel=2)
70 DeprecationWarning, stacklevel=2)
66 if isinstance(lines, str):
71 if isinstance(lines, str):
67 lines = [lines]
72 lines = [lines]
68 for line in lines:
73 for line in lines:
69 self.write(line)
74 self.write(line)
70
75
71 # This class used to have a writeln method, but regular files and streams
76 # This class used to have a writeln method, but regular files and streams
72 # in Python don't have this method. We need to keep this completely
77 # in Python don't have this method. We need to keep this completely
73 # compatible so we removed it.
78 # compatible so we removed it.
74
79
75 @property
80 @property
76 def closed(self):
81 def closed(self):
77 return self.stream.closed
82 return self.stream.closed
78
83
79 def close(self):
84 def close(self):
80 pass
85 pass
81
86
82 # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr
87 # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr
83 devnull = open(os.devnull, 'w')
88 devnull = open(os.devnull, 'w')
84 atexit.register(devnull.close)
89 atexit.register(devnull.close)
85
90
86 # io.std* are deprecated, but don't show our own deprecation warnings
91 # io.std* are deprecated, but don't show our own deprecation warnings
87 # during initialization of the deprecated API.
92 # during initialization of the deprecated API.
88 with warnings.catch_warnings():
93 with warnings.catch_warnings():
89 warnings.simplefilter('ignore', DeprecationWarning)
94 warnings.simplefilter('ignore', DeprecationWarning)
90 stdin = IOStream(sys.stdin, fallback=devnull)
95 stdin = IOStream(sys.stdin, fallback=devnull)
91 stdout = IOStream(sys.stdout, fallback=devnull)
96 stdout = IOStream(sys.stdout, fallback=devnull)
92 stderr = IOStream(sys.stderr, fallback=devnull)
97 stderr = IOStream(sys.stderr, fallback=devnull)
93
98
94 class Tee(object):
99 class Tee(object):
95 """A class to duplicate an output stream to stdout/err.
100 """A class to duplicate an output stream to stdout/err.
96
101
97 This works in a manner very similar to the Unix 'tee' command.
102 This works in a manner very similar to the Unix 'tee' command.
98
103
99 When the object is closed or deleted, it closes the original file given to
104 When the object is closed or deleted, it closes the original file given to
100 it for duplication.
105 it for duplication.
101 """
106 """
102 # Inspired by:
107 # Inspired by:
103 # http://mail.python.org/pipermail/python-list/2007-May/442737.html
108 # http://mail.python.org/pipermail/python-list/2007-May/442737.html
104
109
105 def __init__(self, file_or_name, mode="w", channel='stdout'):
110 def __init__(self, file_or_name, mode="w", channel='stdout'):
106 """Construct a new Tee object.
111 """Construct a new Tee object.
107
112
108 Parameters
113 Parameters
109 ----------
114 ----------
110 file_or_name : filename or open filehandle (writable)
115 file_or_name : filename or open filehandle (writable)
111 File that will be duplicated
116 File that will be duplicated
112
117
113 mode : optional, valid mode for open().
118 mode : optional, valid mode for open().
114 If a filename was give, open with this mode.
119 If a filename was give, open with this mode.
115
120
116 channel : str, one of ['stdout', 'stderr']
121 channel : str, one of ['stdout', 'stderr']
117 """
122 """
118 if channel not in ['stdout', 'stderr']:
123 if channel not in ['stdout', 'stderr']:
119 raise ValueError('Invalid channel spec %s' % channel)
124 raise ValueError('Invalid channel spec %s' % channel)
120
125
121 if hasattr(file_or_name, 'write') and hasattr(file_or_name, 'seek'):
126 if hasattr(file_or_name, 'write') and hasattr(file_or_name, 'seek'):
122 self.file = file_or_name
127 self.file = file_or_name
123 else:
128 else:
124 self.file = open(file_or_name, mode)
129 self.file = open(file_or_name, mode)
125 self.channel = channel
130 self.channel = channel
126 self.ostream = getattr(sys, channel)
131 self.ostream = getattr(sys, channel)
127 setattr(sys, channel, self)
132 setattr(sys, channel, self)
128 self._closed = False
133 self._closed = False
129
134
130 def close(self):
135 def close(self):
131 """Close the file and restore the channel."""
136 """Close the file and restore the channel."""
132 self.flush()
137 self.flush()
133 setattr(sys, self.channel, self.ostream)
138 setattr(sys, self.channel, self.ostream)
134 self.file.close()
139 self.file.close()
135 self._closed = True
140 self._closed = True
136
141
137 def write(self, data):
142 def write(self, data):
138 """Write data to both channels."""
143 """Write data to both channels."""
139 self.file.write(data)
144 self.file.write(data)
140 self.ostream.write(data)
145 self.ostream.write(data)
141 self.ostream.flush()
146 self.ostream.flush()
142
147
143 def flush(self):
148 def flush(self):
144 """Flush both channels."""
149 """Flush both channels."""
145 self.file.flush()
150 self.file.flush()
146 self.ostream.flush()
151 self.ostream.flush()
147
152
148 def __del__(self):
153 def __del__(self):
149 if not self._closed:
154 if not self._closed:
150 self.close()
155 self.close()
151
156
152
157
153 def ask_yes_no(prompt, default=None, interrupt=None):
158 def ask_yes_no(prompt, default=None, interrupt=None):
154 """Asks a question and returns a boolean (y/n) answer.
159 """Asks a question and returns a boolean (y/n) answer.
155
160
156 If default is given (one of 'y','n'), it is used if the user input is
161 If default is given (one of 'y','n'), it is used if the user input is
157 empty. If interrupt is given (one of 'y','n'), it is used if the user
162 empty. If interrupt is given (one of 'y','n'), it is used if the user
158 presses Ctrl-C. Otherwise the question is repeated until an answer is
163 presses Ctrl-C. Otherwise the question is repeated until an answer is
159 given.
164 given.
160
165
161 An EOF is treated as the default answer. If there is no default, an
166 An EOF is treated as the default answer. If there is no default, an
162 exception is raised to prevent infinite loops.
167 exception is raised to prevent infinite loops.
163
168
164 Valid answers are: y/yes/n/no (match is not case sensitive)."""
169 Valid answers are: y/yes/n/no (match is not case sensitive)."""
165
170
166 answers = {'y':True,'n':False,'yes':True,'no':False}
171 answers = {'y':True,'n':False,'yes':True,'no':False}
167 ans = None
172 ans = None
168 while ans not in answers.keys():
173 while ans not in answers.keys():
169 try:
174 try:
170 ans = input(prompt+' ').lower()
175 ans = input(prompt+' ').lower()
171 if not ans: # response was an empty string
176 if not ans: # response was an empty string
172 ans = default
177 ans = default
173 except KeyboardInterrupt:
178 except KeyboardInterrupt:
174 if interrupt:
179 if interrupt:
175 ans = interrupt
180 ans = interrupt
176 print("\r")
181 print("\r")
177 except EOFError:
182 except EOFError:
178 if default in answers.keys():
183 if default in answers.keys():
179 ans = default
184 ans = default
180 print()
185 print()
181 else:
186 else:
182 raise
187 raise
183
188
184 return answers[ans]
189 return answers[ans]
185
190
186
191
187 def temp_pyfile(src, ext='.py'):
192 def temp_pyfile(src, ext='.py'):
188 """Make a temporary python file, return filename and filehandle.
193 """Make a temporary python file, return filename and filehandle.
189
194
190 Parameters
195 Parameters
191 ----------
196 ----------
192 src : string or list of strings (no need for ending newlines if list)
197 src : string or list of strings (no need for ending newlines if list)
193 Source code to be written to the file.
198 Source code to be written to the file.
194
199
195 ext : optional, string
200 ext : optional, string
196 Extension for the generated file.
201 Extension for the generated file.
197
202
198 Returns
203 Returns
199 -------
204 -------
200 (filename, open filehandle)
205 (filename, open filehandle)
201 It is the caller's responsibility to close the open file and unlink it.
206 It is the caller's responsibility to close the open file and unlink it.
202 """
207 """
203 fname = tempfile.mkstemp(ext)[1]
208 fname = tempfile.mkstemp(ext)[1]
204 f = open(fname,'w')
209 f = open(fname,'w')
205 f.write(src)
210 f.write(src)
206 f.flush()
211 f.flush()
207 return fname, f
212 return fname, f
208
213
209 def atomic_writing(*args, **kwargs):
214 def atomic_writing(*args, **kwargs):
210 """DEPRECATED: moved to notebook.services.contents.fileio"""
215 """DEPRECATED: moved to notebook.services.contents.fileio"""
211 warn("IPython.utils.io.atomic_writing has moved to notebook.services.contents.fileio", stacklevel=2)
216 warn("IPython.utils.io.atomic_writing has moved to notebook.services.contents.fileio", stacklevel=2)
212 from notebook.services.contents.fileio import atomic_writing
217 from notebook.services.contents.fileio import atomic_writing
213 return atomic_writing(*args, **kwargs)
218 return atomic_writing(*args, **kwargs)
214
219
215 def raw_print(*args, **kw):
220 def raw_print(*args, **kw):
216 """Raw print to sys.__stdout__, otherwise identical interface to print()."""
221 """Raw print to sys.__stdout__, otherwise identical interface to print()."""
217
222
218 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
223 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
219 file=sys.__stdout__)
224 file=sys.__stdout__)
220 sys.__stdout__.flush()
225 sys.__stdout__.flush()
221
226
222
227
223 def raw_print_err(*args, **kw):
228 def raw_print_err(*args, **kw):
224 """Raw print to sys.__stderr__, otherwise identical interface to print()."""
229 """Raw print to sys.__stderr__, otherwise identical interface to print()."""
225
230
226 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
231 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
227 file=sys.__stderr__)
232 file=sys.__stderr__)
228 sys.__stderr__.flush()
233 sys.__stderr__.flush()
229
234
230
235
231 # Short aliases for quick debugging, do NOT use these in production code.
236 # Short aliases for quick debugging, do NOT use these in production code.
232 rprint = raw_print
237 rprint = raw_print
233 rprinte = raw_print_err
238 rprinte = raw_print_err
234
239
235
240
236 def unicode_std_stream(stream='stdout'):
241 def unicode_std_stream(stream='stdout'):
237 """DEPRECATED, moved to nbconvert.utils.io"""
242 """DEPRECATED, moved to nbconvert.utils.io"""
238 warn("IPython.utils.io.unicode_std_stream has moved to nbconvert.utils.io", stacklevel=2)
243 warn("IPython.utils.io.unicode_std_stream has moved to nbconvert.utils.io", stacklevel=2)
239 from nbconvert.utils.io import unicode_std_stream
244 from nbconvert.utils.io import unicode_std_stream
240 return unicode_std_stream(stream)
245 return unicode_std_stream(stream)
@@ -1,81 +1,94 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """Tests for io.py"""
2 """Tests for io.py"""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7
7
8 import io as stdlib_io
8 import io as stdlib_io
9 import os.path
9 import os.path
10 import stat
10 import stat
11 import sys
11 import sys
12 from io import StringIO
12 from io import StringIO
13
13
14 from subprocess import Popen, PIPE
14 from subprocess import Popen, PIPE
15 import unittest
15 import unittest
16
16
17 import nose.tools as nt
17 import nose.tools as nt
18
18
19 from IPython.testing.decorators import skipif, skip_win32
19 from IPython.testing.decorators import skipif, skip_win32
20 from IPython.utils.io import Tee, capture_output
20 from IPython.utils.io import IOStream, Tee, capture_output
21 from IPython.utils.py3compat import doctest_refactor_print
21 from IPython.utils.py3compat import doctest_refactor_print
22 from IPython.utils.tempdir import TemporaryDirectory
22 from IPython.utils.tempdir import TemporaryDirectory
23
23
24
24
25 def test_tee_simple():
25 def test_tee_simple():
26 "Very simple check with stdout only"
26 "Very simple check with stdout only"
27 chan = StringIO()
27 chan = StringIO()
28 text = 'Hello'
28 text = 'Hello'
29 tee = Tee(chan, channel='stdout')
29 tee = Tee(chan, channel='stdout')
30 print(text, file=chan)
30 print(text, file=chan)
31 nt.assert_equal(chan.getvalue(), text+"\n")
31 nt.assert_equal(chan.getvalue(), text+"\n")
32
32
33
33
34 class TeeTestCase(unittest.TestCase):
34 class TeeTestCase(unittest.TestCase):
35
35
36 def tchan(self, channel, check='close'):
36 def tchan(self, channel, check='close'):
37 trap = StringIO()
37 trap = StringIO()
38 chan = StringIO()
38 chan = StringIO()
39 text = 'Hello'
39 text = 'Hello'
40
40
41 std_ori = getattr(sys, channel)
41 std_ori = getattr(sys, channel)
42 setattr(sys, channel, trap)
42 setattr(sys, channel, trap)
43
43
44 tee = Tee(chan, channel=channel)
44 tee = Tee(chan, channel=channel)
45 print(text, end='', file=chan)
45 print(text, end='', file=chan)
46 setattr(sys, channel, std_ori)
46 setattr(sys, channel, std_ori)
47 trap_val = trap.getvalue()
47 trap_val = trap.getvalue()
48 nt.assert_equal(chan.getvalue(), text)
48 nt.assert_equal(chan.getvalue(), text)
49 if check=='close':
49 if check=='close':
50 tee.close()
50 tee.close()
51 else:
51 else:
52 del tee
52 del tee
53
53
54 def test(self):
54 def test(self):
55 for chan in ['stdout', 'stderr']:
55 for chan in ['stdout', 'stderr']:
56 for check in ['close', 'del']:
56 for check in ['close', 'del']:
57 self.tchan(chan, check)
57 self.tchan(chan, check)
58
58
59 def test_io_init():
59 def test_io_init():
60 """Test that io.stdin/out/err exist at startup"""
60 """Test that io.stdin/out/err exist at startup"""
61 for name in ('stdin', 'stdout', 'stderr'):
61 for name in ('stdin', 'stdout', 'stderr'):
62 cmd = doctest_refactor_print("from IPython.utils import io;print io.%s.__class__"%name)
62 cmd = doctest_refactor_print("from IPython.utils import io;print io.%s.__class__"%name)
63 p = Popen([sys.executable, '-c', cmd],
63 p = Popen([sys.executable, '-c', cmd],
64 stdout=PIPE)
64 stdout=PIPE)
65 p.wait()
65 p.wait()
66 classname = p.stdout.read().strip().decode('ascii')
66 classname = p.stdout.read().strip().decode('ascii')
67 # __class__ is a reference to the class object in Python 3, so we can't
67 # __class__ is a reference to the class object in Python 3, so we can't
68 # just test for string equality.
68 # just test for string equality.
69 assert 'IPython.utils.io.IOStream' in classname, classname
69 assert 'IPython.utils.io.IOStream' in classname, classname
70
70
71 def test_IOStream_init():
72 """IOStream initializes from a file-like object missing attributes. """
73 # Cause a failure from getattr and dir(). (Issue #6386)
74 class BadStringIO(StringIO):
75 def __dir__(self):
76 attrs = super(StringIO, self).__dir__()
77 attrs.append('name')
78 return attrs
79
80 iostream = IOStream(BadStringIO())
81 iostream.write('hi, bad iostream\n')
82 assert not hasattr(iostream, 'name')
83
71 def test_capture_output():
84 def test_capture_output():
72 """capture_output() context works"""
85 """capture_output() context works"""
73
86
74 with capture_output() as io:
87 with capture_output() as io:
75 print('hi, stdout')
88 print('hi, stdout')
76 print('hi, stderr', file=sys.stderr)
89 print('hi, stderr', file=sys.stderr)
77
90
78 nt.assert_equal(io.stdout, 'hi, stdout\n')
91 nt.assert_equal(io.stdout, 'hi, stdout\n')
79 nt.assert_equal(io.stderr, 'hi, stderr\n')
92 nt.assert_equal(io.stderr, 'hi, stderr\n')
80
93
81
94
General Comments 0
You need to be logged in to leave comments. Login now