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