##// END OF EJS Templates
- Small fix to pexpect to prevent unhandled exceptions at Python shutdown...
fperez -
Show More

The requested changes are too big and content was truncated. Show full diff

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