##// END OF EJS Templates
Fix up and move old irunner tests into proper test suite.
Fernando Perez -
Show More
@@ -1,441 +1,441 b''
1 1 #!/usr/bin/env python
2 2 """Module for interactively running scripts.
3 3
4 4 This module implements classes for interactively running scripts written for
5 5 any system with a prompt which can be matched by a regexp suitable for
6 6 pexpect. It can be used to run as if they had been typed up interactively, an
7 7 arbitrary series of commands for the target system.
8 8
9 9 The module includes classes ready for IPython (with the default prompts),
10 10 plain Python and SAGE, but making a new one is trivial. To see how to use it,
11 11 simply run the module as a script:
12 12
13 13 ./irunner.py --help
14 14
15 15
16 16 This is an extension of Ken Schutte <kschutte-AT-csail.mit.edu>'s script
17 17 contributed on the ipython-user list:
18 18
19 19 http://scipy.net/pipermail/ipython-user/2006-May/001705.html
20 20
21 21
22 22 NOTES:
23 23
24 24 - This module requires pexpect, available in most linux distros, or which can
25 25 be downloaded from
26 26
27 27 http://pexpect.sourceforge.net
28 28
29 29 - Because pexpect only works under Unix or Windows-Cygwin, this has the same
30 30 limitations. This means that it will NOT work under native windows Python.
31 31 """
32 32
33 33 # Stdlib imports
34 34 import optparse
35 35 import os
36 36 import sys
37 37
38 38 # Third-party modules.
39 39 import pexpect
40 40
41 41 # Global usage strings, to avoid indentation issues when typing it below.
42 42 USAGE = """
43 43 Interactive script runner, type: %s
44 44
45 45 runner [opts] script_name
46 46 """
47 47
48 48 def pexpect_monkeypatch():
49 49 """Patch pexpect to prevent unhandled exceptions at VM teardown.
50 50
51 51 Calling this function will monkeypatch the pexpect.spawn class and modify
52 52 its __del__ method to make it more robust in the face of failures that can
53 53 occur if it is called when the Python VM is shutting down.
54 54
55 55 Since Python may fire __del__ methods arbitrarily late, it's possible for
56 56 them to execute during the teardown of the Python VM itself. At this
57 57 point, various builtin modules have been reset to None. Thus, the call to
58 58 self.close() will trigger an exception because it tries to call os.close(),
59 59 and os is now None.
60 60 """
61 61
62 62 if pexpect.__version__[:3] >= '2.2':
63 63 # No need to patch, fix is already the upstream version.
64 64 return
65 65
66 66 def __del__(self):
67 67 """This makes sure that no system resources are left open.
68 68 Python only garbage collects Python objects. OS file descriptors
69 69 are not Python objects, so they must be handled explicitly.
70 70 If the child file descriptor was opened outside of this class
71 71 (passed to the constructor) then this does not close it.
72 72 """
73 73 if not self.closed:
74 74 try:
75 75 self.close()
76 76 except AttributeError:
77 77 pass
78 78
79 79 pexpect.spawn.__del__ = __del__
80 80
81 81 pexpect_monkeypatch()
82 82
83 83 # The generic runner class
84 84 class InteractiveRunner(object):
85 85 """Class to run a sequence of commands through an interactive program."""
86 86
87 87 def __init__(self,program,prompts,args=None,out=sys.stdout,echo=True):
88 88 """Construct a runner.
89 89
90 90 Inputs:
91 91
92 92 - program: command to execute the given program.
93 93
94 94 - prompts: a list of patterns to match as valid prompts, in the
95 95 format used by pexpect. This basically means that it can be either
96 96 a string (to be compiled as a regular expression) or a list of such
97 97 (it must be a true list, as pexpect does type checks).
98 98
99 99 If more than one prompt is given, the first is treated as the main
100 100 program prompt and the others as 'continuation' prompts, like
101 101 python's. This means that blank lines in the input source are
102 102 ommitted when the first prompt is matched, but are NOT ommitted when
103 103 the continuation one matches, since this is how python signals the
104 104 end of multiline input interactively.
105 105
106 106 Optional inputs:
107 107
108 108 - args(None): optional list of strings to pass as arguments to the
109 109 child program.
110 110
111 111 - out(sys.stdout): if given, an output stream to be used when writing
112 112 output. The only requirement is that it must have a .write() method.
113 113
114 114 Public members not parameterized in the constructor:
115 115
116 116 - delaybeforesend(0): Newer versions of pexpect have a delay before
117 117 sending each new input. For our purposes here, it's typically best
118 118 to just set this to zero, but if you encounter reliability problems
119 119 or want an interactive run to pause briefly at each prompt, just
120 120 increase this value (it is measured in seconds). Note that this
121 121 variable is not honored at all by older versions of pexpect.
122 122 """
123 123
124 124 self.program = program
125 125 self.prompts = prompts
126 126 if args is None: args = []
127 127 self.args = args
128 128 self.out = out
129 129 self.echo = echo
130 130 # Other public members which we don't make as parameters, but which
131 131 # users may occasionally want to tweak
132 132 self.delaybeforesend = 0
133 133
134 134 # Create child process and hold on to it so we don't have to re-create
135 135 # for every single execution call
136 136 c = self.child = pexpect.spawn(self.program,self.args,timeout=None)
137 137 c.delaybeforesend = self.delaybeforesend
138 138 # pexpect hard-codes the terminal size as (24,80) (rows,columns).
139 139 # This causes problems because any line longer than 80 characters gets
140 140 # completely overwrapped on the printed outptut (even though
141 141 # internally the code runs fine). We reset this to 99 rows X 200
142 142 # columns (arbitrarily chosen), which should avoid problems in all
143 143 # reasonable cases.
144 144 c.setwinsize(99,200)
145 145
146 146 def close(self):
147 147 """close child process"""
148 148
149 149 self.child.close()
150 150
151 151 def run_file(self,fname,interact=False,get_output=False):
152 152 """Run the given file interactively.
153 153
154 154 Inputs:
155 155
156 156 -fname: name of the file to execute.
157 157
158 158 See the run_source docstring for the meaning of the optional
159 159 arguments."""
160 160
161 161 fobj = open(fname,'r')
162 162 try:
163 163 out = self.run_source(fobj,interact,get_output)
164 164 finally:
165 165 fobj.close()
166 166 if get_output:
167 167 return out
168 168
169 169 def run_source(self,source,interact=False,get_output=False):
170 170 """Run the given source code interactively.
171 171
172 172 Inputs:
173 173
174 174 - source: a string of code to be executed, or an open file object we
175 175 can iterate over.
176 176
177 177 Optional inputs:
178 178
179 179 - interact(False): if true, start to interact with the running
180 180 program at the end of the script. Otherwise, just exit.
181 181
182 182 - get_output(False): if true, capture the output of the child process
183 183 (filtering the input commands out) and return it as a string.
184 184
185 185 Returns:
186 186 A string containing the process output, but only if requested.
187 187 """
188 188
189 189 # if the source is a string, chop it up in lines so we can iterate
190 190 # over it just as if it were an open file.
191 191 if not isinstance(source,file):
192 192 source = source.splitlines(True)
193 193
194 194 if self.echo:
195 195 # normalize all strings we write to use the native OS line
196 196 # separators.
197 197 linesep = os.linesep
198 198 stdwrite = self.out.write
199 199 write = lambda s: stdwrite(s.replace('\r\n',linesep))
200 200 else:
201 201 # Quiet mode, all writes are no-ops
202 202 write = lambda s: None
203 203
204 204 c = self.child
205 205 prompts = c.compile_pattern_list(self.prompts)
206 206 prompt_idx = c.expect_list(prompts)
207 207
208 208 # Flag whether the script ends normally or not, to know whether we can
209 209 # do anything further with the underlying process.
210 210 end_normal = True
211 211
212 212 # If the output was requested, store it in a list for return at the end
213 213 if get_output:
214 214 output = []
215 215 store_output = output.append
216 216
217 217 for cmd in source:
218 218 # skip blank lines for all matches to the 'main' prompt, while the
219 219 # secondary prompts do not
220 220 if prompt_idx==0 and \
221 221 (cmd.isspace() or cmd.lstrip().startswith('#')):
222 222 write(cmd)
223 223 continue
224 224
225 225 # write('AFTER: '+c.after) # dbg
226 226 write(c.after)
227 227 c.send(cmd)
228 228 try:
229 229 prompt_idx = c.expect_list(prompts)
230 230 except pexpect.EOF:
231 231 # this will happen if the child dies unexpectedly
232 232 write(c.before)
233 233 end_normal = False
234 234 break
235 235
236 236 write(c.before)
237 237
238 238 # With an echoing process, the output we get in c.before contains
239 239 # the command sent, a newline, and then the actual process output
240 240 if get_output:
241 241 store_output(c.before[len(cmd+'\n'):])
242 242 #write('CMD: <<%s>>' % cmd) # dbg
243 243 #write('OUTPUT: <<%s>>' % output[-1]) # dbg
244 244
245 245 self.out.flush()
246 246 if end_normal:
247 247 if interact:
248 248 c.send('\n')
249 249 print '<< Starting interactive mode >>',
250 250 try:
251 251 c.interact()
252 252 except OSError:
253 253 # This is what fires when the child stops. Simply print a
254 254 # newline so the system prompt is aligned. The extra
255 255 # space is there to make sure it gets printed, otherwise
256 256 # OS buffering sometimes just suppresses it.
257 257 write(' \n')
258 258 self.out.flush()
259 259 else:
260 260 if interact:
261 261 e="Further interaction is not possible: child process is dead."
262 262 print >> sys.stderr, e
263 263
264 264 # Leave the child ready for more input later on, otherwise select just
265 265 # hangs on the second invocation.
266 266 c.send('\n')
267 267
268 268 # Return any requested output
269 269 if get_output:
270 270 return ''.join(output)
271 271
272 272 def main(self,argv=None):
273 273 """Run as a command-line script."""
274 274
275 275 parser = optparse.OptionParser(usage=USAGE % self.__class__.__name__)
276 276 newopt = parser.add_option
277 277 newopt('-i','--interact',action='store_true',default=False,
278 278 help='Interact with the program after the script is run.')
279 279
280 280 opts,args = parser.parse_args(argv)
281 281
282 282 if len(args) != 1:
283 283 print >> sys.stderr,"You must supply exactly one file to run."
284 284 sys.exit(1)
285 285
286 286 self.run_file(args[0],opts.interact)
287 287
288 288
289 289 # Specific runners for particular programs
290 290 class IPythonRunner(InteractiveRunner):
291 291 """Interactive IPython runner.
292 292
293 293 This initalizes IPython in 'nocolor' mode for simplicity. This lets us
294 294 avoid having to write a regexp that matches ANSI sequences, though pexpect
295 295 does support them. If anyone contributes patches for ANSI color support,
296 296 they will be welcome.
297 297
298 298 It also sets the prompts manually, since the prompt regexps for
299 299 pexpect need to be matched to the actual prompts, so user-customized
300 300 prompts would break this.
301 301 """
302 302
303 303 def __init__(self,program = 'ipython',args=None,out=sys.stdout,echo=True):
304 304 """New runner, optionally passing the ipython command to use."""
305 305
306 306 args0 = ['--colors','NoColor',
307 307 '-pi1','In [\\#]: ',
308 308 '-pi2',' .\\D.: ',
309 '--noterm-title',
310 '--no-auto-indent']
309 '--no-term-title',
310 '--no-autoindent']
311 311 if args is None: args = args0
312 312 else: args = args0 + args
313 313 prompts = [r'In \[\d+\]: ',r' \.*: ']
314 314 InteractiveRunner.__init__(self,program,prompts,args,out,echo)
315 315
316 316
317 317 class PythonRunner(InteractiveRunner):
318 318 """Interactive Python runner."""
319 319
320 320 def __init__(self,program='python',args=None,out=sys.stdout,echo=True):
321 321 """New runner, optionally passing the python command to use."""
322 322
323 323 prompts = [r'>>> ',r'\.\.\. ']
324 324 InteractiveRunner.__init__(self,program,prompts,args,out,echo)
325 325
326 326
327 327 class SAGERunner(InteractiveRunner):
328 328 """Interactive SAGE runner.
329 329
330 330 WARNING: this runner only works if you manually configure your SAGE copy
331 331 to use 'colors NoColor' in the ipythonrc config file, since currently the
332 332 prompt matching regexp does not identify color sequences."""
333 333
334 334 def __init__(self,program='sage',args=None,out=sys.stdout,echo=True):
335 335 """New runner, optionally passing the sage command to use."""
336 336
337 337 prompts = ['sage: ',r'\s*\.\.\. ']
338 338 InteractiveRunner.__init__(self,program,prompts,args,out,echo)
339 339
340 340
341 341 class RunnerFactory(object):
342 342 """Code runner factory.
343 343
344 344 This class provides an IPython code runner, but enforces that only one
345 345 runner is ever instantiated. The runner is created based on the extension
346 346 of the first file to run, and it raises an exception if a runner is later
347 347 requested for a different extension type.
348 348
349 349 This ensures that we don't generate example files for doctest with a mix of
350 350 python and ipython syntax.
351 351 """
352 352
353 353 def __init__(self,out=sys.stdout):
354 354 """Instantiate a code runner."""
355 355
356 356 self.out = out
357 357 self.runner = None
358 358 self.runnerClass = None
359 359
360 360 def _makeRunner(self,runnerClass):
361 361 self.runnerClass = runnerClass
362 362 self.runner = runnerClass(out=self.out)
363 363 return self.runner
364 364
365 365 def __call__(self,fname):
366 366 """Return a runner for the given filename."""
367 367
368 368 if fname.endswith('.py'):
369 369 runnerClass = PythonRunner
370 370 elif fname.endswith('.ipy'):
371 371 runnerClass = IPythonRunner
372 372 else:
373 373 raise ValueError('Unknown file type for Runner: %r' % fname)
374 374
375 375 if self.runner is None:
376 376 return self._makeRunner(runnerClass)
377 377 else:
378 378 if runnerClass==self.runnerClass:
379 379 return self.runner
380 380 else:
381 381 e='A runner of type %r can not run file %r' % \
382 382 (self.runnerClass,fname)
383 383 raise ValueError(e)
384 384
385 385
386 386 # Global usage string, to avoid indentation issues if typed in a function def.
387 387 MAIN_USAGE = """
388 388 %prog [options] file_to_run
389 389
390 390 This is an interface to the various interactive runners available in this
391 391 module. If you want to pass specific options to one of the runners, you need
392 392 to first terminate the main options with a '--', and then provide the runner's
393 393 options. For example:
394 394
395 395 irunner.py --python -- --help
396 396
397 397 will pass --help to the python runner. Similarly,
398 398
399 399 irunner.py --ipython -- --interact script.ipy
400 400
401 401 will run the script.ipy file under the IPython runner, and then will start to
402 402 interact with IPython at the end of the script (instead of exiting).
403 403
404 404 The already implemented runners are listed below; adding one for a new program
405 405 is a trivial task, see the source for examples.
406 406
407 407 WARNING: the SAGE runner only works if you manually configure your SAGE copy
408 408 to use 'colors NoColor' in the ipythonrc config file, since currently the
409 409 prompt matching regexp does not identify color sequences.
410 410 """
411 411
412 412 def main():
413 413 """Run as a command-line script."""
414 414
415 415 parser = optparse.OptionParser(usage=MAIN_USAGE)
416 416 newopt = parser.add_option
417 417 parser.set_defaults(mode='ipython')
418 418 newopt('--ipython',action='store_const',dest='mode',const='ipython',
419 419 help='IPython interactive runner (default).')
420 420 newopt('--python',action='store_const',dest='mode',const='python',
421 421 help='Python interactive runner.')
422 422 newopt('--sage',action='store_const',dest='mode',const='sage',
423 423 help='SAGE interactive runner.')
424 424
425 425 opts,args = parser.parse_args()
426 426 runners = dict(ipython=IPythonRunner,
427 427 python=PythonRunner,
428 428 sage=SAGERunner)
429 429
430 430 try:
431 431 ext = os.path.splitext(args[0])[-1]
432 432 except IndexError:
433 433 ext = ''
434 434 modes = {'.ipy':'ipython',
435 435 '.py':'python',
436 436 '.sage':'sage'}
437 437 mode = modes.get(ext,opts.mode)
438 438 runners[mode]().main(args)
439 439
440 440 if __name__ == '__main__':
441 441 main()
@@ -1,168 +1,162 b''
1 #!/usr/bin/env python
2 1 """Test suite for the irunner module.
3 2
4 3 Not the most elegant or fine-grained, but it does cover at least the bulk
5 4 functionality."""
6 5
7 6 # Global to make tests extra verbose and help debugging
8 7 VERBOSE = True
9 8
10 9 # stdlib imports
11 10 import cStringIO as StringIO
12 11 import sys
13 12 import unittest
14 13
15 14 # IPython imports
16 from IPython import irunner
15 from IPython.lib import irunner
17 16
18 17 # Testing code begins
19 18 class RunnerTestCase(unittest.TestCase):
20 19
21 20 def setUp(self):
22 21 self.out = StringIO.StringIO()
23 22 #self.out = sys.stdout
24 23
25 24 def _test_runner(self,runner,source,output):
26 25 """Test that a given runner's input/output match."""
27 26
28 27 runner.run_source(source)
29 28 out = self.out.getvalue()
30 29 #out = ''
31 30 # this output contains nasty \r\n lineends, and the initial ipython
32 # banner. clean it up for comparison
33 output_l = output.split()
34 out_l = out.split()
31 # banner. clean it up for comparison, removing lines of whitespace
32 output_l = [l for l in output.splitlines() if l and not l.isspace()]
33 out_l = [l for l in out.splitlines() if l and not l.isspace()]
35 34 mismatch = 0
36 #if len(output_l) != len(out_l):
37 # self.fail('mismatch in number of lines')
35 if len(output_l) != len(out_l):
36 self.fail('mismatch in number of lines')
38 37 for n in range(len(output_l)):
39 38 # Do a line-by-line comparison
40 39 ol1 = output_l[n].strip()
41 40 ol2 = out_l[n].strip()
42 41 if ol1 != ol2:
43 42 mismatch += 1
44 43 if VERBOSE:
45 44 print '<<< line %s does not match:' % n
46 45 print repr(ol1)
47 46 print repr(ol2)
48 47 print '>>>'
49 48 self.assert_(mismatch==0,'Number of mismatched lines: %s' %
50 49 mismatch)
51 50
52 51 def testIPython(self):
53 52 """Test the IPython runner."""
54 53 source = """
55 54 print 'hello, this is python'
56
57 55 # some more code
58 56 x=1;y=2
59 57 x+y**2
60 58
61 59 # An example of autocall functionality
62 60 from math import *
63 61 autocall 1
64 62 cos pi
65 63 autocall 0
66 64 cos pi
67 65 cos(pi)
68 66
69 67 for i in range(5):
70 68 print i,
71 69
72 70 print "that's all folks!"
73 71
74 72 %Exit
75 73 """
76 74 output = """\
77 75 In [1]: print 'hello, this is python'
78 76 hello, this is python
79 77
80 78
81 79 # some more code
82 80 In [2]: x=1;y=2
83 81
84 82 In [3]: x+y**2
85 83 Out[3]: 5
86 84
87 85
88 86 # An example of autocall functionality
89 87 In [4]: from math import *
90 88
91 89 In [5]: autocall 1
92 90 Automatic calling is: Smart
93 91
94 92 In [6]: cos pi
95 93 ------> cos(pi)
96 94 Out[6]: -1.0
97 95
98 96 In [7]: autocall 0
99 97 Automatic calling is: OFF
100 98
101 99 In [8]: cos pi
102 ------------------------------------------------------------
103 100 File "<ipython console>", line 1
104 101 cos pi
105 102 ^
106 <type 'exceptions.SyntaxError'>: invalid syntax
103 SyntaxError: invalid syntax
107 104
108 105
109 106 In [9]: cos(pi)
110 107 Out[9]: -1.0
111 108
112 109
113 110 In [10]: for i in range(5):
114 111 ....: print i,
115 112 ....:
116 113 0 1 2 3 4
117 114
118 115 In [11]: print "that's all folks!"
119 116 that's all folks!
120 117
121 118
122 119 In [12]: %Exit
123 120 """
124 121 runner = irunner.IPythonRunner(out=self.out)
125 122 self._test_runner(runner,source,output)
126 123
127 124 def testPython(self):
128 125 """Test the Python runner."""
129 126 runner = irunner.PythonRunner(out=self.out)
130 127 source = """
131 128 print 'hello, this is python'
132 129
133 130 # some more code
134 131 x=1;y=2
135 132 x+y**2
136 133
137 134 from math import *
138 135 cos(pi)
139 136
140 137 for i in range(5):
141 138 print i,
142 139
143 140 print "that's all folks!"
144 141 """
145 142 output = """\
146 143 >>> print 'hello, this is python'
147 144 hello, this is python
148 145
149 146 # some more code
150 147 >>> x=1;y=2
151 148 >>> x+y**2
152 149 5
153 150
154 151 >>> from math import *
155 152 >>> cos(pi)
156 153 -1.0
157 154
158 155 >>> for i in range(5):
159 156 ... print i,
160 157 ...
161 158 0 1 2 3 4
162 159 >>> print "that's all folks!"
163 160 that's all folks!
164 161 """
165 162 self._test_runner(runner,source,output)
166
167 if __name__ == '__main__':
168 unittest.main()
General Comments 0
You need to be logged in to leave comments. Login now