2
"""Module for interactively running scripts.
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
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.
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,
11
simply run the module as a script:
16
This is an extension of Ken Schutte <kschutte-AT-csail.mit.edu>'s script
17
contributed on the ipython-user list:
19
http://scipy.net/pipermail/ipython-user/2006-May/001705.html
24
- This module requires pexpect, available in most linux distros, or which can
27
http://pexpect.sourceforge.net
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.
38
# Third-party modules.
41
# Global usage strings, to avoid indentation issues when typing it below.
43
Interactive script runner, type: %s
45
runner [opts] script_name
48
def pexpect_monkeypatch():
49
"""Patch pexpect to prevent unhandled exceptions at VM teardown.
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.
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(),
62
if pexpect.__version__[:3] >= '2.2':
63
# No need to patch, fix is already the upstream version.
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.
76
except AttributeError:
79
pexpect.spawn.__del__ = __del__
83
# The generic runner class
84
class InteractiveRunner(object):
85
"""Class to run a sequence of commands through an interactive program."""
87
def __init__(self,program,prompts,args=None,out=sys.stdout,echo=True):
88
"""Construct a runner.
92
- program: command to execute the given program.
94
- prompts: a list of patterns to match as valid prompts, in the
95
format used by pexpect. This basically means that it can be either
96
a string (to be compiled as a regular expression) or a list of such
97
(it must be a true list, as pexpect does type checks).
99
If more than one prompt is given, the first is treated as the main
100
program prompt and the others as 'continuation' prompts, like
101
python's. This means that blank lines in the input source are
102
ommitted when the first prompt is matched, but are NOT ommitted when
103
the continuation one matches, since this is how python signals the
104
end of multiline input interactively.
108
- args(None): optional list of strings to pass as arguments to the
111
- out(sys.stdout): if given, an output stream to be used when writing
112
output. The only requirement is that it must have a .write() method.
114
Public members not parameterized in the constructor:
116
- delaybeforesend(0): Newer versions of pexpect have a delay before
117
sending each new input. For our purposes here, it's typically best
118
to just set this to zero, but if you encounter reliability problems
119
or want an interactive run to pause briefly at each prompt, just
120
increase this value (it is measured in seconds). Note that this
121
variable is not honored at all by older versions of pexpect.
124
self.program = program
125
self.prompts = prompts
126
if args is None: args = []
130
# Other public members which we don't make as parameters, but which
131
# users may occasionally want to tweak
132
self.delaybeforesend = 0
134
# Create child process and hold on to it so we don't have to re-create
135
# for every single execution call
136
c = self.child = pexpect.spawn(self.program,self.args,timeout=None)
137
c.delaybeforesend = self.delaybeforesend
138
# pexpect hard-codes the terminal size as (24,80) (rows,columns).
139
# This causes problems because any line longer than 80 characters gets
140
# completely overwrapped on the printed outptut (even though
141
# internally the code runs fine). We reset this to 99 rows X 200
142
# columns (arbitrarily chosen), which should avoid problems in all
147
"""close child process"""
151
def run_file(self,fname,interact=False,get_output=False):
152
"""Run the given file interactively.
156
-fname: name of the file to execute.
158
See the run_source docstring for the meaning of the optional
161
fobj = open(fname,'r')
163
out = self.run_source(fobj,interact,get_output)
169
def run_source(self,source,interact=False,get_output=False):
170
"""Run the given source code interactively.
174
- source: a string of code to be executed, or an open file object we
179
- interact(False): if true, start to interact with the running
180
program at the end of the script. Otherwise, just exit.
182
- get_output(False): if true, capture the output of the child process
183
(filtering the input commands out) and return it as a string.
186
A string containing the process output, but only if requested.
189
# if the source is a string, chop it up in lines so we can iterate
190
# over it just as if it were an open file.
191
if not isinstance(source,file):
192
source = source.splitlines(True)
195
# normalize all strings we write to use the native OS line
198
stdwrite = self.out.write
199
write = lambda s: stdwrite(s.replace('\r\n',linesep))
201
# Quiet mode, all writes are no-ops
202
write = lambda s: None
205
prompts = c.compile_pattern_list(self.prompts)
206
prompt_idx = c.expect_list(prompts)
208
# Flag whether the script ends normally or not, to know whether we can
209
# do anything further with the underlying process.
212
# If the output was requested, store it in a list for return at the end
215
store_output = output.append
218
# skip blank lines for all matches to the 'main' prompt, while the
219
# secondary prompts do not
220
if prompt_idx==0 and \
221
(cmd.isspace() or cmd.lstrip().startswith('#')):
225
#write('AFTER: '+c.after) # dbg
229
prompt_idx = c.expect_list(prompts)
231
# this will happen if the child dies unexpectedly
238
# With an echoing process, the output we get in c.before contains
239
# the command sent, a newline, and then the actual process output
241
store_output(c.before[len(cmd+'\n'):])
242
#write('CMD: <<%s>>' % cmd) # dbg
243
#write('OUTPUT: <<%s>>' % output[-1]) # dbg
249
print '<< Starting interactive mode >>',
253
# This is what fires when the child stops. Simply print a
254
# newline so the system prompt is aligned. The extra
255
# space is there to make sure it gets printed, otherwise
256
# OS buffering sometimes just suppresses it.
261
e="Further interaction is not possible: child process is dead."
262
print >> sys.stderr, e
264
# Leave the child ready for more input later on, otherwise select just
265
# hangs on the second invocation.
268
# Return any requested output
270
return ''.join(output)
272
def main(self,argv=None):
273
"""Run as a command-line script."""
275
parser = optparse.OptionParser(usage=USAGE % self.__class__.__name__)
276
newopt = parser.add_option
277
newopt('-i','--interact',action='store_true',default=False,
278
help='Interact with the program after the script is run.')
280
opts,args = parser.parse_args(argv)
283
print >> sys.stderr,"You must supply exactly one file to run."
286
self.run_file(args[0],opts.interact)
289
# Specific runners for particular programs
290
class IPythonRunner(InteractiveRunner):
291
"""Interactive IPython runner.
293
This initalizes IPython in 'nocolor' mode for simplicity. This lets us
294
avoid having to write a regexp that matches ANSI sequences, though pexpect
295
does support them. If anyone contributes patches for ANSI color support,
296
they will be welcome.
298
It also sets the prompts manually, since the prompt regexps for
299
pexpect need to be matched to the actual prompts, so user-customized
300
prompts would break this.
303
def __init__(self,program = 'ipython',args=None,out=sys.stdout,echo=True):
304
"""New runner, optionally passing the ipython command to use."""
306
args0 = ['-colors','NoColor',
311
if args is None: args = args0
312
else: args = args0 + args
313
prompts = [r'In \[\d+\]: ',r' \.*: ']
314
InteractiveRunner.__init__(self,program,prompts,args,out,echo)
317
class PythonRunner(InteractiveRunner):
318
"""Interactive Python runner."""
320
def __init__(self,program='python',args=None,out=sys.stdout,echo=True):
321
"""New runner, optionally passing the python command to use."""
323
prompts = [r'>>> ',r'\.\.\. ']
324
InteractiveRunner.__init__(self,program,prompts,args,out,echo)
327
class SAGERunner(InteractiveRunner):
328
"""Interactive SAGE runner.
330
WARNING: this runner only works if you manually configure your SAGE copy
331
to use 'colors NoColor' in the ipythonrc config file, since currently the
332
prompt matching regexp does not identify color sequences."""
334
def __init__(self,program='sage',args=None,out=sys.stdout,echo=True):
335
"""New runner, optionally passing the sage command to use."""
337
prompts = ['sage: ',r'\s*\.\.\. ']
338
InteractiveRunner.__init__(self,program,prompts,args,out,echo)
341
class RunnerFactory(object):
342
"""Code runner factory.
344
This class provides an IPython code runner, but enforces that only one
345
runner is ever instantiated. The runner is created based on the extension
346
of the first file to run, and it raises an exception if a runner is later
347
requested for a different extension type.
349
This ensures that we don't generate example files for doctest with a mix of
350
python and ipython syntax.
353
def __init__(self,out=sys.stdout):
354
"""Instantiate a code runner."""
358
self.runnerClass = None
360
def _makeRunner(self,runnerClass):
361
self.runnerClass = runnerClass
362
self.runner = runnerClass(out=self.out)
365
def __call__(self,fname):
366
"""Return a runner for the given filename."""
368
if fname.endswith('.py'):
369
runnerClass = PythonRunner
370
elif fname.endswith('.ipy'):
371
runnerClass = IPythonRunner
373
raise ValueError('Unknown file type for Runner: %r' % fname)
375
if self.runner is None:
376
return self._makeRunner(runnerClass)
378
if runnerClass==self.runnerClass:
381
e='A runner of type %r can not run file %r' % \
382
(self.runnerClass,fname)
386
# Global usage string, to avoid indentation issues if typed in a function def.
388
%prog [options] file_to_run
390
This is an interface to the various interactive runners available in this
391
module. If you want to pass specific options to one of the runners, you need
392
to first terminate the main options with a '--', and then provide the runner's
393
options. For example:
395
irunner.py --python -- --help
397
will pass --help to the python runner. Similarly,
399
irunner.py --ipython -- --interact script.ipy
401
will run the script.ipy file under the IPython runner, and then will start to
402
interact with IPython at the end of the script (instead of exiting).
404
The already implemented runners are listed below; adding one for a new program
405
is a trivial task, see the source for examples.
407
WARNING: the SAGE runner only works if you manually configure your SAGE copy
408
to use 'colors NoColor' in the ipythonrc config file, since currently the
409
prompt matching regexp does not identify color sequences.
413
"""Run as a command-line script."""
415
parser = optparse.OptionParser(usage=MAIN_USAGE)
416
newopt = parser.add_option
417
parser.set_defaults(mode='ipython')
418
newopt('--ipython',action='store_const',dest='mode',const='ipython',
419
help='IPython interactive runner (default).')
420
newopt('--python',action='store_const',dest='mode',const='python',
421
help='Python interactive runner.')
422
newopt('--sage',action='store_const',dest='mode',const='sage',
423
help='SAGE interactive runner.')
425
opts,args = parser.parse_args()
426
runners = dict(ipython=IPythonRunner,
431
ext = os.path.splitext(args[0])[-1]
434
modes = {'.ipy':'ipython',
437
mode = modes.get(ext,opts.mode)
438
runners[mode]().main(args)
440
if __name__ == '__main__':