##// END OF EJS Templates
Follow Fernando's suggestions.
Thomas Kluyver -
Show More
@@ -1,332 +1,337 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Paging capabilities for IPython.core
3 Paging capabilities for IPython.core
4
4
5 Authors:
5 Authors:
6
6
7 * Brian Granger
7 * Brian Granger
8 * Fernando Perez
8 * Fernando Perez
9
9
10 Notes
10 Notes
11 -----
11 -----
12
12
13 For now this uses ipapi, so it can't be in IPython.utils. If we can get
13 For now this uses ipapi, so it can't be in IPython.utils. If we can get
14 rid of that dependency, we could move it there.
14 rid of that dependency, we could move it there.
15 -----
15 -----
16 """
16 """
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Copyright (C) 2008-2009 The IPython Development Team
19 # Copyright (C) 2008-2009 The IPython Development Team
20 #
20 #
21 # Distributed under the terms of the BSD License. The full license is in
21 # Distributed under the terms of the BSD License. The full license is in
22 # the file COPYING, distributed as part of this software.
22 # the file COPYING, distributed as part of this software.
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24
24
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 # Imports
26 # Imports
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28
28
29 import os
29 import os
30 import re
30 import re
31 import sys
31 import sys
32 import tempfile
32 import tempfile
33
33
34 from IPython.core import ipapi
34 from IPython.core import ipapi
35 from IPython.core.error import TryNext
35 from IPython.core.error import TryNext
36 from IPython.utils.cursesimport import use_curses
36 from IPython.utils.cursesimport import use_curses
37 from IPython.utils.data import chop
37 from IPython.utils.data import chop
38 from IPython.utils import io
38 from IPython.utils import io
39 from IPython.utils.process import system
39 from IPython.utils.process import system
40 from IPython.utils.terminal import get_terminal_size
40 from IPython.utils.terminal import get_terminal_size
41
41
42
42
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44 # Classes and functions
44 # Classes and functions
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46
46
47 esc_re = re.compile(r"(\x1b[^m]+m)")
47 esc_re = re.compile(r"(\x1b[^m]+m)")
48
48
49 def page_dumb(strng, start=0, screen_lines=25):
49 def page_dumb(strng, start=0, screen_lines=25):
50 """Very dumb 'pager' in Python, for when nothing else works.
50 """Very dumb 'pager' in Python, for when nothing else works.
51
51
52 Only moves forward, same interface as page(), except for pager_cmd and
52 Only moves forward, same interface as page(), except for pager_cmd and
53 mode."""
53 mode."""
54
54
55 out_ln = strng.splitlines()[start:]
55 out_ln = strng.splitlines()[start:]
56 screens = chop(out_ln,screen_lines-1)
56 screens = chop(out_ln,screen_lines-1)
57 if len(screens) == 1:
57 if len(screens) == 1:
58 print >>io.stdout, os.linesep.join(screens[0])
58 print >>io.stdout, os.linesep.join(screens[0])
59 else:
59 else:
60 last_escape = ""
60 last_escape = ""
61 for scr in screens[0:-1]:
61 for scr in screens[0:-1]:
62 hunk = os.linesep.join(scr)
62 hunk = os.linesep.join(scr)
63 print >>io.stdout, last_escape + hunk
63 print >>io.stdout, last_escape + hunk
64 if not page_more():
64 if not page_more():
65 return
65 return
66 esc_list = esc_re.findall(hunk)
66 esc_list = esc_re.findall(hunk)
67 if len(esc_list) > 0:
67 if len(esc_list) > 0:
68 last_escape = esc_list[-1]
68 last_escape = esc_list[-1]
69 print >>io.stdout, last_escape + os.linesep.join(screens[-1])
69 print >>io.stdout, last_escape + os.linesep.join(screens[-1])
70
70
71 def _detect_screen_size(use_curses, screen_lines_def):
71 def _detect_screen_size(use_curses, screen_lines_def):
72 """Attempt to work out the number of lines on the screen.
73
74 This is called by page(). It can raise an error (e.g. when run in the
75 test suite), so it's separated out so it can easily be called in a try block.
76 """
72 if (TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5':
77 if (TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5':
73 local_use_curses = use_curses
78 local_use_curses = use_curses
74 else:
79 else:
75 # curses causes problems on many terminals other than xterm, and
80 # curses causes problems on many terminals other than xterm, and
76 # some termios calls lock up on Sun OS5.
81 # some termios calls lock up on Sun OS5.
77 local_use_curses = False
82 local_use_curses = False
78 if local_use_curses:
83 if local_use_curses:
79 import termios
84 import termios
80 import curses
85 import curses
81 # There is a bug in curses, where *sometimes* it fails to properly
86 # There is a bug in curses, where *sometimes* it fails to properly
82 # initialize, and then after the endwin() call is made, the
87 # initialize, and then after the endwin() call is made, the
83 # terminal is left in an unusable state. Rather than trying to
88 # terminal is left in an unusable state. Rather than trying to
84 # check everytime for this (by requesting and comparing termios
89 # check everytime for this (by requesting and comparing termios
85 # flags each time), we just save the initial terminal state and
90 # flags each time), we just save the initial terminal state and
86 # unconditionally reset it every time. It's cheaper than making
91 # unconditionally reset it every time. It's cheaper than making
87 # the checks.
92 # the checks.
88 term_flags = termios.tcgetattr(sys.stdout)
93 term_flags = termios.tcgetattr(sys.stdout)
89
94
90 # Curses modifies the stdout buffer size by default, which messes
95 # Curses modifies the stdout buffer size by default, which messes
91 # up Python's normal stdout buffering. This would manifest itself
96 # up Python's normal stdout buffering. This would manifest itself
92 # to IPython users as delayed printing on stdout after having used
97 # to IPython users as delayed printing on stdout after having used
93 # the pager.
98 # the pager.
94 #
99 #
95 # We can prevent this by manually setting the NCURSES_NO_SETBUF
100 # We can prevent this by manually setting the NCURSES_NO_SETBUF
96 # environment variable. For more details, see:
101 # environment variable. For more details, see:
97 # http://bugs.python.org/issue10144
102 # http://bugs.python.org/issue10144
98 NCURSES_NO_SETBUF = os.environ.get('NCURSES_NO_SETBUF', None)
103 NCURSES_NO_SETBUF = os.environ.get('NCURSES_NO_SETBUF', None)
99 os.environ['NCURSES_NO_SETBUF'] = ''
104 os.environ['NCURSES_NO_SETBUF'] = ''
100
105
101 # Proceed with curses initialization
106 # Proceed with curses initialization
102 scr = curses.initscr()
107 scr = curses.initscr()
103 screen_lines_real,screen_cols = scr.getmaxyx()
108 screen_lines_real,screen_cols = scr.getmaxyx()
104 curses.endwin()
109 curses.endwin()
105
110
106 # Restore environment
111 # Restore environment
107 if NCURSES_NO_SETBUF is None:
112 if NCURSES_NO_SETBUF is None:
108 del os.environ['NCURSES_NO_SETBUF']
113 del os.environ['NCURSES_NO_SETBUF']
109 else:
114 else:
110 os.environ['NCURSES_NO_SETBUF'] = NCURSES_NO_SETBUF
115 os.environ['NCURSES_NO_SETBUF'] = NCURSES_NO_SETBUF
111
116
112 # Restore terminal state in case endwin() didn't.
117 # Restore terminal state in case endwin() didn't.
113 termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags)
118 termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags)
114 # Now we have what we needed: the screen size in rows/columns
119 # Now we have what we needed: the screen size in rows/columns
115 return screen_lines_real
120 return screen_lines_real
116 #print '***Screen size:',screen_lines_real,'lines x',\
121 #print '***Screen size:',screen_lines_real,'lines x',\
117 #screen_cols,'columns.' # dbg
122 #screen_cols,'columns.' # dbg
118 else:
123 else:
119 return screen_lines_def
124 return screen_lines_def
120
125
121 def page(strng, start=0, screen_lines=0, pager_cmd=None):
126 def page(strng, start=0, screen_lines=0, pager_cmd=None):
122 """Print a string, piping through a pager after a certain length.
127 """Print a string, piping through a pager after a certain length.
123
128
124 The screen_lines parameter specifies the number of *usable* lines of your
129 The screen_lines parameter specifies the number of *usable* lines of your
125 terminal screen (total lines minus lines you need to reserve to show other
130 terminal screen (total lines minus lines you need to reserve to show other
126 information).
131 information).
127
132
128 If you set screen_lines to a number <=0, page() will try to auto-determine
133 If you set screen_lines to a number <=0, page() will try to auto-determine
129 your screen size and will only use up to (screen_size+screen_lines) for
134 your screen size and will only use up to (screen_size+screen_lines) for
130 printing, paging after that. That is, if you want auto-detection but need
135 printing, paging after that. That is, if you want auto-detection but need
131 to reserve the bottom 3 lines of the screen, use screen_lines = -3, and for
136 to reserve the bottom 3 lines of the screen, use screen_lines = -3, and for
132 auto-detection without any lines reserved simply use screen_lines = 0.
137 auto-detection without any lines reserved simply use screen_lines = 0.
133
138
134 If a string won't fit in the allowed lines, it is sent through the
139 If a string won't fit in the allowed lines, it is sent through the
135 specified pager command. If none given, look for PAGER in the environment,
140 specified pager command. If none given, look for PAGER in the environment,
136 and ultimately default to less.
141 and ultimately default to less.
137
142
138 If no system pager works, the string is sent through a 'dumb pager'
143 If no system pager works, the string is sent through a 'dumb pager'
139 written in python, very simplistic.
144 written in python, very simplistic.
140 """
145 """
141
146
142 # Some routines may auto-compute start offsets incorrectly and pass a
147 # Some routines may auto-compute start offsets incorrectly and pass a
143 # negative value. Offset to 0 for robustness.
148 # negative value. Offset to 0 for robustness.
144 start = max(0, start)
149 start = max(0, start)
145
150
146 # first, try the hook
151 # first, try the hook
147 ip = ipapi.get()
152 ip = ipapi.get()
148 if ip:
153 if ip:
149 try:
154 try:
150 ip.hooks.show_in_pager(strng)
155 ip.hooks.show_in_pager(strng)
151 return
156 return
152 except TryNext:
157 except TryNext:
153 pass
158 pass
154
159
155 # Ugly kludge, but calling curses.initscr() flat out crashes in emacs
160 # Ugly kludge, but calling curses.initscr() flat out crashes in emacs
156 TERM = os.environ.get('TERM','dumb')
161 TERM = os.environ.get('TERM','dumb')
157 if TERM in ['dumb','emacs'] and os.name != 'nt':
162 if TERM in ['dumb','emacs'] and os.name != 'nt':
158 print strng
163 print strng
159 return
164 return
160 # chop off the topmost part of the string we don't want to see
165 # chop off the topmost part of the string we don't want to see
161 str_lines = strng.splitlines()[start:]
166 str_lines = strng.splitlines()[start:]
162 str_toprint = os.linesep.join(str_lines)
167 str_toprint = os.linesep.join(str_lines)
163 num_newlines = len(str_lines)
168 num_newlines = len(str_lines)
164 len_str = len(str_toprint)
169 len_str = len(str_toprint)
165
170
166 # Dumb heuristics to guesstimate number of on-screen lines the string
171 # Dumb heuristics to guesstimate number of on-screen lines the string
167 # takes. Very basic, but good enough for docstrings in reasonable
172 # takes. Very basic, but good enough for docstrings in reasonable
168 # terminals. If someone later feels like refining it, it's not hard.
173 # terminals. If someone later feels like refining it, it's not hard.
169 numlines = max(num_newlines,int(len_str/80)+1)
174 numlines = max(num_newlines,int(len_str/80)+1)
170
175
171 screen_lines_def = get_terminal_size()[1]
176 screen_lines_def = get_terminal_size()[1]
172
177
173 # auto-determine screen size
178 # auto-determine screen size
174 if screen_lines <= 0:
179 if screen_lines <= 0:
175 try:
180 try:
176 screen_lines += _detect_screen_size(use_curses, screen_lines_def)
181 screen_lines += _detect_screen_size(use_curses, screen_lines_def)
177 except Exception:
182 except Exception:
178 print >>io.stdout, str_toprint
183 print >>io.stdout, str_toprint
179 return
184 return
180
185
181 #print 'numlines',numlines,'screenlines',screen_lines # dbg
186 #print 'numlines',numlines,'screenlines',screen_lines # dbg
182 if numlines <= screen_lines :
187 if numlines <= screen_lines :
183 #print '*** normal print' # dbg
188 #print '*** normal print' # dbg
184 print >>io.stdout, str_toprint
189 print >>io.stdout, str_toprint
185 else:
190 else:
186 # Try to open pager and default to internal one if that fails.
191 # Try to open pager and default to internal one if that fails.
187 # All failure modes are tagged as 'retval=1', to match the return
192 # All failure modes are tagged as 'retval=1', to match the return
188 # value of a failed system command. If any intermediate attempt
193 # value of a failed system command. If any intermediate attempt
189 # sets retval to 1, at the end we resort to our own page_dumb() pager.
194 # sets retval to 1, at the end we resort to our own page_dumb() pager.
190 pager_cmd = get_pager_cmd(pager_cmd)
195 pager_cmd = get_pager_cmd(pager_cmd)
191 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
196 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
192 if os.name == 'nt':
197 if os.name == 'nt':
193 if pager_cmd.startswith('type'):
198 if pager_cmd.startswith('type'):
194 # The default WinXP 'type' command is failing on complex strings.
199 # The default WinXP 'type' command is failing on complex strings.
195 retval = 1
200 retval = 1
196 else:
201 else:
197 tmpname = tempfile.mktemp('.txt')
202 tmpname = tempfile.mktemp('.txt')
198 tmpfile = file(tmpname,'wt')
203 tmpfile = file(tmpname,'wt')
199 tmpfile.write(strng)
204 tmpfile.write(strng)
200 tmpfile.close()
205 tmpfile.close()
201 cmd = "%s < %s" % (pager_cmd,tmpname)
206 cmd = "%s < %s" % (pager_cmd,tmpname)
202 if os.system(cmd):
207 if os.system(cmd):
203 retval = 1
208 retval = 1
204 else:
209 else:
205 retval = None
210 retval = None
206 os.remove(tmpname)
211 os.remove(tmpname)
207 else:
212 else:
208 try:
213 try:
209 retval = None
214 retval = None
210 # if I use popen4, things hang. No idea why.
215 # if I use popen4, things hang. No idea why.
211 #pager,shell_out = os.popen4(pager_cmd)
216 #pager,shell_out = os.popen4(pager_cmd)
212 pager = os.popen(pager_cmd,'w')
217 pager = os.popen(pager_cmd,'w')
213 pager.write(strng)
218 pager.write(strng)
214 pager.close()
219 pager.close()
215 retval = pager.close() # success returns None
220 retval = pager.close() # success returns None
216 except IOError,msg: # broken pipe when user quits
221 except IOError,msg: # broken pipe when user quits
217 if msg.args == (32,'Broken pipe'):
222 if msg.args == (32,'Broken pipe'):
218 retval = None
223 retval = None
219 else:
224 else:
220 retval = 1
225 retval = 1
221 except OSError:
226 except OSError:
222 # Other strange problems, sometimes seen in Win2k/cygwin
227 # Other strange problems, sometimes seen in Win2k/cygwin
223 retval = 1
228 retval = 1
224 if retval is not None:
229 if retval is not None:
225 page_dumb(strng,screen_lines=screen_lines)
230 page_dumb(strng,screen_lines=screen_lines)
226
231
227
232
228 def page_file(fname, start=0, pager_cmd=None):
233 def page_file(fname, start=0, pager_cmd=None):
229 """Page a file, using an optional pager command and starting line.
234 """Page a file, using an optional pager command and starting line.
230 """
235 """
231
236
232 pager_cmd = get_pager_cmd(pager_cmd)
237 pager_cmd = get_pager_cmd(pager_cmd)
233 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
238 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
234
239
235 try:
240 try:
236 if os.environ['TERM'] in ['emacs','dumb']:
241 if os.environ['TERM'] in ['emacs','dumb']:
237 raise EnvironmentError
242 raise EnvironmentError
238 system(pager_cmd + ' ' + fname)
243 system(pager_cmd + ' ' + fname)
239 except:
244 except:
240 try:
245 try:
241 if start > 0:
246 if start > 0:
242 start -= 1
247 start -= 1
243 page(open(fname).read(),start)
248 page(open(fname).read(),start)
244 except:
249 except:
245 print 'Unable to show file',`fname`
250 print 'Unable to show file',`fname`
246
251
247
252
248 def get_pager_cmd(pager_cmd=None):
253 def get_pager_cmd(pager_cmd=None):
249 """Return a pager command.
254 """Return a pager command.
250
255
251 Makes some attempts at finding an OS-correct one.
256 Makes some attempts at finding an OS-correct one.
252 """
257 """
253 if os.name == 'posix':
258 if os.name == 'posix':
254 default_pager_cmd = 'less -r' # -r for color control sequences
259 default_pager_cmd = 'less -r' # -r for color control sequences
255 elif os.name in ['nt','dos']:
260 elif os.name in ['nt','dos']:
256 default_pager_cmd = 'type'
261 default_pager_cmd = 'type'
257
262
258 if pager_cmd is None:
263 if pager_cmd is None:
259 try:
264 try:
260 pager_cmd = os.environ['PAGER']
265 pager_cmd = os.environ['PAGER']
261 except:
266 except:
262 pager_cmd = default_pager_cmd
267 pager_cmd = default_pager_cmd
263 return pager_cmd
268 return pager_cmd
264
269
265
270
266 def get_pager_start(pager, start):
271 def get_pager_start(pager, start):
267 """Return the string for paging files with an offset.
272 """Return the string for paging files with an offset.
268
273
269 This is the '+N' argument which less and more (under Unix) accept.
274 This is the '+N' argument which less and more (under Unix) accept.
270 """
275 """
271
276
272 if pager in ['less','more']:
277 if pager in ['less','more']:
273 if start:
278 if start:
274 start_string = '+' + str(start)
279 start_string = '+' + str(start)
275 else:
280 else:
276 start_string = ''
281 start_string = ''
277 else:
282 else:
278 start_string = ''
283 start_string = ''
279 return start_string
284 return start_string
280
285
281
286
282 # (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch()
287 # (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch()
283 if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs':
288 if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs':
284 import msvcrt
289 import msvcrt
285 def page_more():
290 def page_more():
286 """ Smart pausing between pages
291 """ Smart pausing between pages
287
292
288 @return: True if need print more lines, False if quit
293 @return: True if need print more lines, False if quit
289 """
294 """
290 io.stdout.write('---Return to continue, q to quit--- ')
295 io.stdout.write('---Return to continue, q to quit--- ')
291 ans = msvcrt.getch()
296 ans = msvcrt.getch()
292 if ans in ("q", "Q"):
297 if ans in ("q", "Q"):
293 result = False
298 result = False
294 else:
299 else:
295 result = True
300 result = True
296 io.stdout.write("\b"*37 + " "*37 + "\b"*37)
301 io.stdout.write("\b"*37 + " "*37 + "\b"*37)
297 return result
302 return result
298 else:
303 else:
299 def page_more():
304 def page_more():
300 ans = raw_input('---Return to continue, q to quit--- ')
305 ans = raw_input('---Return to continue, q to quit--- ')
301 if ans.lower().startswith('q'):
306 if ans.lower().startswith('q'):
302 return False
307 return False
303 else:
308 else:
304 return True
309 return True
305
310
306
311
307 def snip_print(str,width = 75,print_full = 0,header = ''):
312 def snip_print(str,width = 75,print_full = 0,header = ''):
308 """Print a string snipping the midsection to fit in width.
313 """Print a string snipping the midsection to fit in width.
309
314
310 print_full: mode control:
315 print_full: mode control:
311 - 0: only snip long strings
316 - 0: only snip long strings
312 - 1: send to page() directly.
317 - 1: send to page() directly.
313 - 2: snip long strings and ask for full length viewing with page()
318 - 2: snip long strings and ask for full length viewing with page()
314 Return 1 if snipping was necessary, 0 otherwise."""
319 Return 1 if snipping was necessary, 0 otherwise."""
315
320
316 if print_full == 1:
321 if print_full == 1:
317 page(header+str)
322 page(header+str)
318 return 0
323 return 0
319
324
320 print header,
325 print header,
321 if len(str) < width:
326 if len(str) < width:
322 print str
327 print str
323 snip = 0
328 snip = 0
324 else:
329 else:
325 whalf = int((width -5)/2)
330 whalf = int((width -5)/2)
326 print str[:whalf] + ' <...> ' + str[-whalf:]
331 print str[:whalf] + ' <...> ' + str[-whalf:]
327 snip = 1
332 snip = 1
328 if snip and print_full == 2:
333 if snip and print_full == 2:
329 if raw_input(header+' Snipped. View (y/n)? [N]').lower() == 'y':
334 if raw_input(header+' Snipped. View (y/n)? [N]').lower() == 'y':
330 page(str)
335 page(str)
331 return snip
336 return snip
332
337
@@ -1,400 +1,402 b''
1 """Generic testing tools that do NOT depend on Twisted.
1 """Generic testing tools that do NOT depend on Twisted.
2
2
3 In particular, this module exposes a set of top-level assert* functions that
3 In particular, this module exposes a set of top-level assert* functions that
4 can be used in place of nose.tools.assert* in method generators (the ones in
4 can be used in place of nose.tools.assert* in method generators (the ones in
5 nose can not, at least as of nose 0.10.4).
5 nose can not, at least as of nose 0.10.4).
6
6
7 Note: our testing package contains testing.util, which does depend on Twisted
7 Note: our testing package contains testing.util, which does depend on Twisted
8 and provides utilities for tests that manage Deferreds. All testing support
8 and provides utilities for tests that manage Deferreds. All testing support
9 tools that only depend on nose, IPython or the standard library should go here
9 tools that only depend on nose, IPython or the standard library should go here
10 instead.
10 instead.
11
11
12
12
13 Authors
13 Authors
14 -------
14 -------
15 - Fernando Perez <Fernando.Perez@berkeley.edu>
15 - Fernando Perez <Fernando.Perez@berkeley.edu>
16 """
16 """
17
17
18 from __future__ import absolute_import
18 from __future__ import absolute_import
19
19
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21 # Copyright (C) 2009 The IPython Development Team
21 # Copyright (C) 2009 The IPython Development Team
22 #
22 #
23 # Distributed under the terms of the BSD License. The full license is in
23 # Distributed under the terms of the BSD License. The full license is in
24 # the file COPYING, distributed as part of this software.
24 # the file COPYING, distributed as part of this software.
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Imports
28 # Imports
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31 import os
31 import os
32 import re
32 import re
33 import sys
33 import sys
34 import tempfile
34 import tempfile
35
35
36 from contextlib import contextmanager
36 from contextlib import contextmanager
37 from io import StringIO
37 from io import StringIO
38
38
39 try:
39 try:
40 # These tools are used by parts of the runtime, so we make the nose
40 # These tools are used by parts of the runtime, so we make the nose
41 # dependency optional at this point. Nose is a hard dependency to run the
41 # dependency optional at this point. Nose is a hard dependency to run the
42 # test suite, but NOT to use ipython itself.
42 # test suite, but NOT to use ipython itself.
43 import nose.tools as nt
43 import nose.tools as nt
44 has_nose = True
44 has_nose = True
45 except ImportError:
45 except ImportError:
46 has_nose = False
46 has_nose = False
47
47
48 from IPython.config.loader import Config
48 from IPython.config.loader import Config
49 from IPython.utils.process import find_cmd, getoutputerror
49 from IPython.utils.process import find_cmd, getoutputerror
50 from IPython.utils.text import list_strings, getdefaultencoding
50 from IPython.utils.text import list_strings, getdefaultencoding
51 from IPython.utils.io import temp_pyfile, Tee
51 from IPython.utils.io import temp_pyfile, Tee
52 from IPython.utils import py3compat
52 from IPython.utils import py3compat
53
53
54 from . import decorators as dec
54 from . import decorators as dec
55 from . import skipdoctest
55 from . import skipdoctest
56
56
57 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
58 # Globals
58 # Globals
59 #-----------------------------------------------------------------------------
59 #-----------------------------------------------------------------------------
60
60
61 # Make a bunch of nose.tools assert wrappers that can be used in test
61 # Make a bunch of nose.tools assert wrappers that can be used in test
62 # generators. This will expose an assert* function for each one in nose.tools.
62 # generators. This will expose an assert* function for each one in nose.tools.
63
63
64 _tpl = """
64 _tpl = """
65 def %(name)s(*a,**kw):
65 def %(name)s(*a,**kw):
66 return nt.%(name)s(*a,**kw)
66 return nt.%(name)s(*a,**kw)
67 """
67 """
68
68
69 if has_nose:
69 if has_nose:
70 for _x in [a for a in dir(nt) if a.startswith('assert')]:
70 for _x in [a for a in dir(nt) if a.startswith('assert')]:
71 exec _tpl % dict(name=_x)
71 exec _tpl % dict(name=_x)
72
72
73 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
74 # Functions and classes
74 # Functions and classes
75 #-----------------------------------------------------------------------------
75 #-----------------------------------------------------------------------------
76
76
77 # The docstring for full_path doctests differently on win32 (different path
77 # The docstring for full_path doctests differently on win32 (different path
78 # separator) so just skip the doctest there. The example remains informative.
78 # separator) so just skip the doctest there. The example remains informative.
79 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
79 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
80
80
81 @doctest_deco
81 @doctest_deco
82 def full_path(startPath,files):
82 def full_path(startPath,files):
83 """Make full paths for all the listed files, based on startPath.
83 """Make full paths for all the listed files, based on startPath.
84
84
85 Only the base part of startPath is kept, since this routine is typically
85 Only the base part of startPath is kept, since this routine is typically
86 used with a script's __file__ variable as startPath. The base of startPath
86 used with a script's __file__ variable as startPath. The base of startPath
87 is then prepended to all the listed files, forming the output list.
87 is then prepended to all the listed files, forming the output list.
88
88
89 Parameters
89 Parameters
90 ----------
90 ----------
91 startPath : string
91 startPath : string
92 Initial path to use as the base for the results. This path is split
92 Initial path to use as the base for the results. This path is split
93 using os.path.split() and only its first component is kept.
93 using os.path.split() and only its first component is kept.
94
94
95 files : string or list
95 files : string or list
96 One or more files.
96 One or more files.
97
97
98 Examples
98 Examples
99 --------
99 --------
100
100
101 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
101 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
102 ['/foo/a.txt', '/foo/b.txt']
102 ['/foo/a.txt', '/foo/b.txt']
103
103
104 >>> full_path('/foo',['a.txt','b.txt'])
104 >>> full_path('/foo',['a.txt','b.txt'])
105 ['/a.txt', '/b.txt']
105 ['/a.txt', '/b.txt']
106
106
107 If a single file is given, the output is still a list:
107 If a single file is given, the output is still a list:
108 >>> full_path('/foo','a.txt')
108 >>> full_path('/foo','a.txt')
109 ['/a.txt']
109 ['/a.txt']
110 """
110 """
111
111
112 files = list_strings(files)
112 files = list_strings(files)
113 base = os.path.split(startPath)[0]
113 base = os.path.split(startPath)[0]
114 return [ os.path.join(base,f) for f in files ]
114 return [ os.path.join(base,f) for f in files ]
115
115
116
116
117 def parse_test_output(txt):
117 def parse_test_output(txt):
118 """Parse the output of a test run and return errors, failures.
118 """Parse the output of a test run and return errors, failures.
119
119
120 Parameters
120 Parameters
121 ----------
121 ----------
122 txt : str
122 txt : str
123 Text output of a test run, assumed to contain a line of one of the
123 Text output of a test run, assumed to contain a line of one of the
124 following forms::
124 following forms::
125 'FAILED (errors=1)'
125 'FAILED (errors=1)'
126 'FAILED (failures=1)'
126 'FAILED (failures=1)'
127 'FAILED (errors=1, failures=1)'
127 'FAILED (errors=1, failures=1)'
128
128
129 Returns
129 Returns
130 -------
130 -------
131 nerr, nfail: number of errors and failures.
131 nerr, nfail: number of errors and failures.
132 """
132 """
133
133
134 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
134 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
135 if err_m:
135 if err_m:
136 nerr = int(err_m.group(1))
136 nerr = int(err_m.group(1))
137 nfail = 0
137 nfail = 0
138 return nerr, nfail
138 return nerr, nfail
139
139
140 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
140 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
141 if fail_m:
141 if fail_m:
142 nerr = 0
142 nerr = 0
143 nfail = int(fail_m.group(1))
143 nfail = int(fail_m.group(1))
144 return nerr, nfail
144 return nerr, nfail
145
145
146 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
146 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
147 re.MULTILINE)
147 re.MULTILINE)
148 if both_m:
148 if both_m:
149 nerr = int(both_m.group(1))
149 nerr = int(both_m.group(1))
150 nfail = int(both_m.group(2))
150 nfail = int(both_m.group(2))
151 return nerr, nfail
151 return nerr, nfail
152
152
153 # If the input didn't match any of these forms, assume no error/failures
153 # If the input didn't match any of these forms, assume no error/failures
154 return 0, 0
154 return 0, 0
155
155
156
156
157 # So nose doesn't think this is a test
157 # So nose doesn't think this is a test
158 parse_test_output.__test__ = False
158 parse_test_output.__test__ = False
159
159
160
160
161 def default_argv():
161 def default_argv():
162 """Return a valid default argv for creating testing instances of ipython"""
162 """Return a valid default argv for creating testing instances of ipython"""
163
163
164 return ['--quick', # so no config file is loaded
164 return ['--quick', # so no config file is loaded
165 # Other defaults to minimize side effects on stdout
165 # Other defaults to minimize side effects on stdout
166 '--colors=NoColor', '--no-term-title','--no-banner',
166 '--colors=NoColor', '--no-term-title','--no-banner',
167 '--autocall=0']
167 '--autocall=0']
168
168
169
169
170 def default_config():
170 def default_config():
171 """Return a config object with good defaults for testing."""
171 """Return a config object with good defaults for testing."""
172 config = Config()
172 config = Config()
173 config.TerminalInteractiveShell.colors = 'NoColor'
173 config.TerminalInteractiveShell.colors = 'NoColor'
174 config.TerminalTerminalInteractiveShell.term_title = False,
174 config.TerminalTerminalInteractiveShell.term_title = False,
175 config.TerminalInteractiveShell.autocall = 0
175 config.TerminalInteractiveShell.autocall = 0
176 config.HistoryManager.hist_file = tempfile.mktemp(u'test_hist.sqlite')
176 config.HistoryManager.hist_file = tempfile.mktemp(u'test_hist.sqlite')
177 config.HistoryManager.db_cache_size = 10000
177 config.HistoryManager.db_cache_size = 10000
178 return config
178 return config
179
179
180
180
181 def ipexec(fname, options=None):
181 def ipexec(fname, options=None):
182 """Utility to call 'ipython filename'.
182 """Utility to call 'ipython filename'.
183
183
184 Starts IPython witha minimal and safe configuration to make startup as fast
184 Starts IPython witha minimal and safe configuration to make startup as fast
185 as possible.
185 as possible.
186
186
187 Note that this starts IPython in a subprocess!
187 Note that this starts IPython in a subprocess!
188
188
189 Parameters
189 Parameters
190 ----------
190 ----------
191 fname : str
191 fname : str
192 Name of file to be executed (should have .py or .ipy extension).
192 Name of file to be executed (should have .py or .ipy extension).
193
193
194 options : optional, list
194 options : optional, list
195 Extra command-line flags to be passed to IPython.
195 Extra command-line flags to be passed to IPython.
196
196
197 Returns
197 Returns
198 -------
198 -------
199 (stdout, stderr) of ipython subprocess.
199 (stdout, stderr) of ipython subprocess.
200 """
200 """
201 if options is None: options = []
201 if options is None: options = []
202
202
203 # For these subprocess calls, eliminate all prompt printing so we only see
203 # For these subprocess calls, eliminate all prompt printing so we only see
204 # output from script execution
204 # output from script execution
205 prompt_opts = [ '--InteractiveShell.prompt_in1=""',
205 prompt_opts = [ '--InteractiveShell.prompt_in1=""',
206 '--InteractiveShell.prompt_in2=""',
206 '--InteractiveShell.prompt_in2=""',
207 '--InteractiveShell.prompt_out=""'
207 '--InteractiveShell.prompt_out=""'
208 ]
208 ]
209 cmdargs = ' '.join(default_argv() + prompt_opts + options)
209 cmdargs = ' '.join(default_argv() + prompt_opts + options)
210
210
211 _ip = get_ipython()
211 _ip = get_ipython()
212 test_dir = os.path.dirname(__file__)
212 test_dir = os.path.dirname(__file__)
213
213
214 ipython_cmd = find_cmd('ipython3' if py3compat.PY3 else 'ipython')
214 ipython_cmd = find_cmd('ipython3' if py3compat.PY3 else 'ipython')
215 # Absolute path for filename
215 # Absolute path for filename
216 full_fname = os.path.join(test_dir, fname)
216 full_fname = os.path.join(test_dir, fname)
217 full_cmd = '%s %s %s' % (ipython_cmd, cmdargs, full_fname)
217 full_cmd = '%s %s %s' % (ipython_cmd, cmdargs, full_fname)
218 #print >> sys.stderr, 'FULL CMD:', full_cmd # dbg
218 #print >> sys.stderr, 'FULL CMD:', full_cmd # dbg
219 out = getoutputerror(full_cmd)
219 out = getoutputerror(full_cmd)
220 # `import readline` causes 'ESC[?1034h' to be the first output sometimes,
220 # `import readline` causes 'ESC[?1034h' to be the first output sometimes,
221 # so strip that off the front of the first line if it is found
221 # so strip that off the front of the first line if it is found
222 if out:
222 if out:
223 first = out[0]
223 first = out[0]
224 m = re.match(r'\x1b\[[^h]+h', first)
224 m = re.match(r'\x1b\[[^h]+h', first)
225 if m:
225 if m:
226 # strip initial readline escape
226 # strip initial readline escape
227 out = list(out)
227 out = list(out)
228 out[0] = first[len(m.group()):]
228 out[0] = first[len(m.group()):]
229 out = tuple(out)
229 out = tuple(out)
230 return out
230 return out
231
231
232
232
233 def ipexec_validate(fname, expected_out, expected_err='',
233 def ipexec_validate(fname, expected_out, expected_err='',
234 options=None):
234 options=None):
235 """Utility to call 'ipython filename' and validate output/error.
235 """Utility to call 'ipython filename' and validate output/error.
236
236
237 This function raises an AssertionError if the validation fails.
237 This function raises an AssertionError if the validation fails.
238
238
239 Note that this starts IPython in a subprocess!
239 Note that this starts IPython in a subprocess!
240
240
241 Parameters
241 Parameters
242 ----------
242 ----------
243 fname : str
243 fname : str
244 Name of the file to be executed (should have .py or .ipy extension).
244 Name of the file to be executed (should have .py or .ipy extension).
245
245
246 expected_out : str
246 expected_out : str
247 Expected stdout of the process.
247 Expected stdout of the process.
248
248
249 expected_err : optional, str
249 expected_err : optional, str
250 Expected stderr of the process.
250 Expected stderr of the process.
251
251
252 options : optional, list
252 options : optional, list
253 Extra command-line flags to be passed to IPython.
253 Extra command-line flags to be passed to IPython.
254
254
255 Returns
255 Returns
256 -------
256 -------
257 None
257 None
258 """
258 """
259
259
260 import nose.tools as nt
260 import nose.tools as nt
261
261
262 out, err = ipexec(fname)
262 out, err = ipexec(fname)
263 #print 'OUT', out # dbg
263 #print 'OUT', out # dbg
264 #print 'ERR', err # dbg
264 #print 'ERR', err # dbg
265 # If there are any errors, we must check those befor stdout, as they may be
265 # If there are any errors, we must check those befor stdout, as they may be
266 # more informative than simply having an empty stdout.
266 # more informative than simply having an empty stdout.
267 if err:
267 if err:
268 if expected_err:
268 if expected_err:
269 nt.assert_equals(err.strip(), expected_err.strip())
269 nt.assert_equals(err.strip(), expected_err.strip())
270 else:
270 else:
271 raise ValueError('Running file %r produced error: %r' %
271 raise ValueError('Running file %r produced error: %r' %
272 (fname, err))
272 (fname, err))
273 # If no errors or output on stderr was expected, match stdout
273 # If no errors or output on stderr was expected, match stdout
274 nt.assert_equals(out.strip(), expected_out.strip())
274 nt.assert_equals(out.strip(), expected_out.strip())
275
275
276
276
277 class TempFileMixin(object):
277 class TempFileMixin(object):
278 """Utility class to create temporary Python/IPython files.
278 """Utility class to create temporary Python/IPython files.
279
279
280 Meant as a mixin class for test cases."""
280 Meant as a mixin class for test cases."""
281
281
282 def mktmp(self, src, ext='.py'):
282 def mktmp(self, src, ext='.py'):
283 """Make a valid python temp file."""
283 """Make a valid python temp file."""
284 fname, f = temp_pyfile(src, ext)
284 fname, f = temp_pyfile(src, ext)
285 self.tmpfile = f
285 self.tmpfile = f
286 self.fname = fname
286 self.fname = fname
287
287
288 def tearDown(self):
288 def tearDown(self):
289 if hasattr(self, 'tmpfile'):
289 if hasattr(self, 'tmpfile'):
290 # If the tmpfile wasn't made because of skipped tests, like in
290 # If the tmpfile wasn't made because of skipped tests, like in
291 # win32, there's nothing to cleanup.
291 # win32, there's nothing to cleanup.
292 self.tmpfile.close()
292 self.tmpfile.close()
293 try:
293 try:
294 os.unlink(self.fname)
294 os.unlink(self.fname)
295 except:
295 except:
296 # On Windows, even though we close the file, we still can't
296 # On Windows, even though we close the file, we still can't
297 # delete it. I have no clue why
297 # delete it. I have no clue why
298 pass
298 pass
299
299
300 pair_fail_msg = ("Testing {0}\n\n"
300 pair_fail_msg = ("Testing {0}\n\n"
301 "In:\n"
301 "In:\n"
302 " {1!r}\n"
302 " {1!r}\n"
303 "Expected:\n"
303 "Expected:\n"
304 " {2!r}\n"
304 " {2!r}\n"
305 "Got:\n"
305 "Got:\n"
306 " {3!r}\n")
306 " {3!r}\n")
307 def check_pairs(func, pairs):
307 def check_pairs(func, pairs):
308 """Utility function for the common case of checking a function with a
308 """Utility function for the common case of checking a function with a
309 sequence of input/output pairs.
309 sequence of input/output pairs.
310
310
311 Parameters
311 Parameters
312 ----------
312 ----------
313 func : callable
313 func : callable
314 The function to be tested. Should accept a single argument.
314 The function to be tested. Should accept a single argument.
315 pairs : iterable
315 pairs : iterable
316 A list of (input, expected_output) tuples.
316 A list of (input, expected_output) tuples.
317
317
318 Returns
318 Returns
319 -------
319 -------
320 None. Raises an AssertionError if any output does not match the expected
320 None. Raises an AssertionError if any output does not match the expected
321 value.
321 value.
322 """
322 """
323 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
323 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
324 for inp, expected in pairs:
324 for inp, expected in pairs:
325 out = func(inp)
325 out = func(inp)
326 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
326 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
327
327
328
328 if py3compat.PY3:
329 if py3compat.PY3:
329 MyStringIO = StringIO
330 MyStringIO = StringIO
330 else:
331 else:
331 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
332 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
332 # so we need a class that can handle both.
333 # so we need a class that can handle both.
333 class MyStringIO(StringIO):
334 class MyStringIO(StringIO):
334 def write(self, s):
335 def write(self, s):
335 s = py3compat.cast_unicode(s, encoding=getdefaultencoding())
336 s = py3compat.cast_unicode(s, encoding=getdefaultencoding())
336 super(MyStringIO, self).write(s)
337 super(MyStringIO, self).write(s)
337
338
338 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
339 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
339 {2!r}"""
340 {2!r}"""
341
340 class AssertPrints(object):
342 class AssertPrints(object):
341 """Context manager for testing that code prints certain text.
343 """Context manager for testing that code prints certain text.
342
344
343 Examples
345 Examples
344 --------
346 --------
345 >>> with AssertPrints("abc", suppress=False):
347 >>> with AssertPrints("abc", suppress=False):
346 ... print "abcd"
348 ... print "abcd"
347 ... print "def"
349 ... print "def"
348 ...
350 ...
349 abcd
351 abcd
350 def
352 def
351 """
353 """
352 def __init__(self, s, channel='stdout', suppress=True):
354 def __init__(self, s, channel='stdout', suppress=True):
353 self.s = s
355 self.s = s
354 self.channel = channel
356 self.channel = channel
355 self.suppress = suppress
357 self.suppress = suppress
356
358
357 def __enter__(self):
359 def __enter__(self):
358 self.orig_stream = getattr(sys, self.channel)
360 self.orig_stream = getattr(sys, self.channel)
359 self.buffer = MyStringIO()
361 self.buffer = MyStringIO()
360 self.tee = Tee(self.buffer, channel=self.channel)
362 self.tee = Tee(self.buffer, channel=self.channel)
361 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
363 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
362
364
363 def __exit__(self, etype, value, traceback):
365 def __exit__(self, etype, value, traceback):
364 self.tee.flush()
366 self.tee.flush()
365 setattr(sys, self.channel, self.orig_stream)
367 setattr(sys, self.channel, self.orig_stream)
366 printed = self.buffer.getvalue()
368 printed = self.buffer.getvalue()
367 assert self.s in printed, notprinted_msg.format(self.s, self.channel, printed)
369 assert self.s in printed, notprinted_msg.format(self.s, self.channel, printed)
368 return False
370 return False
369
371
370 class AssertNotPrints(AssertPrints):
372 class AssertNotPrints(AssertPrints):
371 """Context manager for checking that certain output *isn't* produced.
373 """Context manager for checking that certain output *isn't* produced.
372
374
373 Counterpart of AssertPrints"""
375 Counterpart of AssertPrints"""
374 def __exit__(self, etype, value, traceback):
376 def __exit__(self, etype, value, traceback):
375 self.tee.flush()
377 self.tee.flush()
376 setattr(sys, self.channel, self.orig_stream)
378 setattr(sys, self.channel, self.orig_stream)
377 printed = self.buffer.getvalue()
379 printed = self.buffer.getvalue()
378 assert self.s not in printed, notprinted_msg.format(self.s, self.channel, printed)
380 assert self.s not in printed, notprinted_msg.format(self.s, self.channel, printed)
379 return False
381 return False
380
382
381 @contextmanager
383 @contextmanager
382 def mute_warn():
384 def mute_warn():
383 from IPython.utils import warn
385 from IPython.utils import warn
384 save_warn = warn.warn
386 save_warn = warn.warn
385 warn.warn = lambda *a, **kw: None
387 warn.warn = lambda *a, **kw: None
386 try:
388 try:
387 yield
389 yield
388 finally:
390 finally:
389 warn.warn = save_warn
391 warn.warn = save_warn
390
392
391 @contextmanager
393 @contextmanager
392 def make_tempfile(name):
394 def make_tempfile(name):
393 """ Create an empty, named, temporary file for the duration of the context.
395 """ Create an empty, named, temporary file for the duration of the context.
394 """
396 """
395 f = open(name, 'w')
397 f = open(name, 'w')
396 f.close()
398 f.close()
397 try:
399 try:
398 yield
400 yield
399 finally:
401 finally:
400 os.unlink(name)
402 os.unlink(name)
General Comments 0
You need to be logged in to leave comments. Login now