##// END OF EJS Templates
New system for interactively running scripts. After a submission by Ken...
fperez -
Show More
@@ -0,0 +1,276 b''
1 #!/usr/bin/env python
2 """Module for interactively running scripts.
3
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.
8
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:
12
13 ./irunner.py --help
14
15
16 This is an extension of Ken Schutte <kschutte-AT-csail.mit.edu>'s script
17 contributed on the ipython-user list:
18
19 http://scipy.net/pipermail/ipython-user/2006-May/001705.html
20
21
22 NOTES:
23
24 - This module requires pexpect, available in most linux distros, or which can
25 be downloaded from
26
27 http://pexpect.sourceforge.net
28
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.
31 """
32
33 # Stdlib imports
34 import optparse
35 import sys
36
37 # Third-party modules.
38 import pexpect
39
40 # Global usage strings, to avoid indentation issues when typing it below.
41 USAGE = """
42 Interactive script runner, type: %s
43
44 runner [opts] script_name
45 """
46
47 # The generic runner class
48 class InteractiveRunner(object):
49 """Class to run a sequence of commands through an interactive program."""
50
51 def __init__(self,program,prompts,args=None):
52 """Construct a runner.
53
54 Inputs:
55
56 - program: command to execute the given program.
57
58 - prompts: a list of patterns to match as valid prompts, in the
59 format used by pexpect. This basically means that it can be either
60 a string (to be compiled as a regular expression) or a list of such
61 (it must be a true list, as pexpect does type checks).
62
63 If more than one prompt is given, the first is treated as the main
64 program prompt and the others as 'continuation' prompts, like
65 python's. This means that blank lines in the input source are
66 ommitted when the first prompt is matched, but are NOT ommitted when
67 the continuation one matches, since this is how python signals the
68 end of multiline input interactively.
69
70 Optional inputs:
71
72 - args(None): optional list of strings to pass as arguments to the
73 child program.
74 """
75
76 self.program = program
77 self.prompts = prompts
78 if args is None: args = []
79 self.args = args
80
81 def run_file(self,fname,interact=False):
82 """Run the given file interactively.
83
84 Inputs:
85
86 -fname: name of the file to execute.
87
88 See the run_source docstring for the meaning of the optional
89 arguments."""
90
91 fobj = open(fname,'r')
92 try:
93 self.run_source(fobj,interact)
94 finally:
95 fobj.close()
96
97 def run_source(self,source,interact=False):
98 """Run the given source code interactively.
99
100 Inputs:
101
102 - source: a string of code to be executed, or an open file object we
103 can iterate over.
104
105 Optional inputs:
106
107 - interact(False): if true, start to interact with the running
108 program at the end of the script. Otherwise, just exit.
109 """
110
111 # if the source is a string, chop it up in lines so we can iterate
112 # over it just as if it were an open file.
113 if not isinstance(source,file):
114 source = source.splitlines(True)
115
116 # grab the true write method of stdout, in case anything later
117 # reassigns sys.stdout, so that we really are writing to the true
118 # stdout and not to something else.
119 write = sys.stdout.write
120
121 c = pexpect.spawn(self.program,self.args,timeout=None)
122
123 prompts = c.compile_pattern_list(self.prompts[0])
124 prompts = c.compile_pattern_list(self.prompts)
125
126 prompt_idx = c.expect_list(prompts)
127 # Flag whether the script ends normally or not, to know whether we can
128 # do anything further with the underlying process.
129 end_normal = True
130 for cmd in source:
131 # skip blank lines for all matches to the 'main' prompt, while the
132 # secondary prompts do not
133 if prompt_idx==0 and cmd.isspace():
134 continue
135
136 write(c.after)
137 c.send(cmd)
138 try:
139 prompt_idx = c.expect_list(prompts)
140 except pexpect.EOF:
141 # this will happen if the child dies unexpectedly
142 write(c.before)
143 end_normal = False
144 break
145 write(c.before)
146
147 if isinstance(source,file):
148 source.close()
149
150 if end_normal:
151 if interact:
152 c.send('\n')
153 print '<< Starting interactive mode >>',
154 try:
155 c.interact()
156 except OSError:
157 # This is what fires when the child stops. Simply print a
158 # newline so the system prompt is alingned. The extra
159 # space is there to make sure it gets printed, otherwise
160 # OS buffering sometimes just suppresses it.
161 write(' \n')
162 sys.stdout.flush()
163 else:
164 c.close()
165 else:
166 if interact:
167 e="Further interaction is not possible: child process is dead."
168 print >> sys.stderr, e
169
170 def main(self,argv=None):
171 """Run as a command-line script."""
172
173 parser = optparse.OptionParser(usage=USAGE % self.__class__.__name__)
174 newopt = parser.add_option
175 newopt('-i','--interact',action='store_true',default=False,
176 help='Interact with the program after the script is run.')
177
178 opts,args = parser.parse_args(argv)
179
180 if len(args) != 1:
181 print >> sys.stderr,"You must supply exactly one file to run."
182 sys.exit(1)
183
184 self.run_file(args[0],opts.interact)
185
186
187 # Specific runners for particular programs
188 class IPythonRunner(InteractiveRunner):
189 """Interactive IPython runner.
190
191 This initalizes IPython in 'nocolor' mode for simplicity. This lets us
192 avoid having to write a regexp that matches ANSI sequences, though pexpect
193 does support them. If anyone contributes patches for ANSI color support,
194 they will be welcome.
195
196 It also sets the prompts manually, since the prompt regexps for
197 pexpect need to be matched to the actual prompts, so user-customized
198 prompts would break this.
199 """
200
201 def __init__(self,program = 'ipython',args=None):
202 """New runner, optionally passing the ipython command to use."""
203
204 args0 = ['-colors','NoColor',
205 '-pi1','In [\\#]: ',
206 '-pi2',' .\\D.: ']
207 if args is None: args = args0
208 else: args = args0 + args
209 prompts = [r'In \[\d+\]: ',r' \.*: ']
210 InteractiveRunner.__init__(self,program,prompts,args)
211
212
213 class PythonRunner(InteractiveRunner):
214 """Interactive Python runner."""
215
216 def __init__(self,program='python',args=None):
217 """New runner, optionally passing the python command to use."""
218
219 prompts = [r'>>> ',r'\.\.\. ']
220 InteractiveRunner.__init__(self,program,prompts,args)
221
222
223 class SAGERunner(InteractiveRunner):
224 """Interactive SAGE runner.
225
226 XXX - This class is currently untested, meant for feedback from the SAGE
227 team. """
228
229 def __init__(self,program='sage',args=None):
230 """New runner, optionally passing the sage command to use."""
231 print 'XXX - This class is currently untested!!!'
232 print 'It is a placeholder, meant for feedback from the SAGE team.'
233
234 prompts = ['sage: ',r'\s*\.\.\. ']
235 InteractiveRunner.__init__(self,program,prompts,args)
236
237 # Global usage string, to avoid indentation issues if typed in a function def.
238 MAIN_USAGE = """
239 %prog [options] file_to_run
240
241 This is an interface to the various interactive runners available in this
242 module. If you want to pass specific options to one of the runners, you need
243 to first terminate the main options with a '--', and then provide the runner's
244 options. For example:
245
246 irunner.py --python -- --help
247
248 will pass --help to the python runner. Similarly,
249
250 irunner.py --ipython -- --log test.log script.ipy
251
252 will run the script.ipy file under the IPython runner, logging all output into
253 the test.log file.
254 """
255
256 def main():
257 """Run as a command-line script."""
258
259 parser = optparse.OptionParser(usage=MAIN_USAGE)
260 newopt = parser.add_option
261 parser.set_defaults(mode='ipython')
262 newopt('--ipython',action='store_const',dest='mode',const='ipython',
263 help='IPython interactive runner (default).')
264 newopt('--python',action='store_const',dest='mode',const='python',
265 help='Python interactive runner.')
266 newopt('--sage',action='store_const',dest='mode',const='sage',
267 help='SAGE interactive runner - UNTESTED.')
268
269 opts,args = parser.parse_args()
270 runners = dict(ipython=IPythonRunner,
271 python=PythonRunner,
272 sage=SAGERunner)
273 runners[opts.mode]().main(args)
274
275 if __name__ == '__main__':
276 main()
@@ -0,0 +1,156 b''
1 #!/usr/bin/env python
2 """Test suite for the irunner module.
3
4 Not the most elegant or fine-grained, but it does cover at least the bulk
5 functionality."""
6
7 # Global to make tests extra verbose and help debugging
8 VERBOSE = True
9
10 # stdlib imports
11 import cStringIO as StringIO
12 import unittest
13
14 # IPython imports
15 from IPython import irunner
16 from IPython.OutputTrap import OutputTrap
17
18 # Testing code begins
19 class RunnerTestCase(unittest.TestCase):
20
21 def _test_runner(self,runner,source,output):
22 """Test that a given runner's input/output match."""
23
24 log = OutputTrap(out_head='',quiet_out=True)
25 log.trap_out()
26 runner.run_source(source)
27 log.release_out()
28 out = log.summary_out()
29 # this output contains nasty \r\n lineends, and the initial ipython
30 # banner. clean it up for comparison
31 output_l = output.split()
32 out_l = out.split()
33 mismatch = 0
34 for n in range(len(output_l)):
35 ol1 = output_l[n].strip()
36 ol2 = out_l[n].strip()
37 if ol1 != ol2:
38 mismatch += 1
39 if VERBOSE:
40 print '<<< line %s does not match:' % n
41 print repr(ol1)
42 print repr(ol2)
43 print '>>>'
44 self.assert_(mismatch==0,'Number of mismatched lines: %s' %
45 mismatch)
46
47 def testIPython(self):
48 """Test the IPython runner."""
49 source = """
50 print 'hello, this is python'
51
52 # some more code
53 x=1;y=2
54 x+y**2
55
56 # An example of autocall functionality
57 from math import *
58 autocall 1
59 cos pi
60 autocall 0
61 cos pi
62 cos(pi)
63
64 for i in range(5):
65 print i,
66
67 print "that's all folks!"
68
69 %Exit
70 """
71 output = """\
72 In [1]: print 'hello, this is python'
73 hello, this is python
74
75 In [2]: # some more code
76
77 In [3]: x=1;y=2
78
79 In [4]: x+y**2
80 Out[4]: 5
81
82 In [5]: # An example of autocall functionality
83
84 In [6]: from math import *
85
86 In [7]: autocall 1
87 Automatic calling is: Smart
88
89 In [8]: cos pi
90 ------> cos(pi)
91 Out[8]: -1.0
92
93 In [9]: autocall 0
94 Automatic calling is: OFF
95
96 In [10]: cos pi
97 ------------------------------------------------------------
98 File "<ipython console>", line 1
99 cos pi
100 ^
101 SyntaxError: invalid syntax
102
103
104 In [11]: cos(pi)
105 Out[11]: -1.0
106
107 In [12]: for i in range(5):
108 ....: print i,
109 ....:
110 0 1 2 3 4
111
112 In [13]: print "that's all folks!"
113 that's all folks!
114
115 In [14]: %Exit"""
116 runner = irunner.IPythonRunner()
117 self._test_runner(runner,source,output)
118
119 def testPython(self):
120 """Test the Python runner."""
121 runner = irunner.PythonRunner()
122 source = """
123 print 'hello, this is python'
124
125 # some more code
126 x=1;y=2
127 x+y**2
128
129 from math import *
130 cos(pi)
131
132 for i in range(5):
133 print i,
134
135 print "that's all folks!"
136 """
137 output = """\
138 >>> print 'hello, this is python'
139 hello, this is python
140 >>> # some more code
141 ... x=1;y=2
142 >>> x+y**2
143 5
144 >>> from math import *
145 >>> cos(pi)
146 -1.0
147 >>> for i in range(5):
148 ... print i,
149 ...
150 0 1 2 3 4
151 >>> print "that's all folks!"
152 that's all folks!"""
153 self._test_runner(runner,source,output)
154
155 if __name__ == '__main__':
156 unittest.main()
@@ -6,7 +6,7 b' Requires Python 2.3 or newer.'
6
6
7 This file contains all the classes and helper functions specific to IPython.
7 This file contains all the classes and helper functions specific to IPython.
8
8
9 $Id: iplib.py 1329 2006-05-26 07:52:45Z fperez $
9 $Id: iplib.py 1332 2006-05-30 01:41:28Z fperez $
10 """
10 """
11
11
12 #*****************************************************************************
12 #*****************************************************************************
@@ -190,7 +190,6 b' class InteractiveShell(object,Magic):'
190 user_ns = None,user_global_ns=None,banner2='',
190 user_ns = None,user_global_ns=None,banner2='',
191 custom_exceptions=((),None),embedded=False):
191 custom_exceptions=((),None),embedded=False):
192
192
193
194 # log system
193 # log system
195 self.logger = Logger(self,logfname='ipython_log.py',logmode='rotate')
194 self.logger = Logger(self,logfname='ipython_log.py',logmode='rotate')
196
195
@@ -6,7 +6,7 b''
6 # the file COPYING, distributed as part of this software.
6 # the file COPYING, distributed as part of this software.
7 #*****************************************************************************
7 #*****************************************************************************
8
8
9 # $Id: usage.py 1301 2006-05-15 17:21:55Z vivainio $
9 # $Id: usage.py 1332 2006-05-30 01:41:28Z fperez $
10
10
11 from IPython import Release
11 from IPython import Release
12 __author__ = '%s <%s>' % Release.authors['Fernando']
12 __author__ = '%s <%s>' % Release.authors['Fernando']
@@ -1,3 +1,15 b''
1 2006-05-29 Fernando Perez <Fernando.Perez@colorado.edu>
2
3 * IPython/irunner.py: New module to run scripts as if manually
4 typed into an interactive environment, based on pexpect. After a
5 submission by Ken Schutte <kschutte-AT-csail.mit.edu> on the
6 ipython-user list. Simple unittests in the tests/ directory.
7
8 * tools/release: add Will Maier, OpenBSD port maintainer, to
9 recepients list. We are now officially part of the OpenBSD ports:
10 http://www.openbsd.org/ports.html ! Many thanks to Will for the
11 work.
12
1 2006-05-26 Fernando Perez <Fernando.Perez@colorado.edu>
13 2006-05-26 Fernando Perez <Fernando.Perez@colorado.edu>
2
14
3 * IPython/ipmaker.py (make_IPython): modify sys.argv fix (below)
15 * IPython/ipmaker.py (make_IPython): modify sys.argv fix (below)
1 NO CONTENT: modified file
NO CONTENT: modified file
@@ -9512,4 +9512,13 b' Belchenko'
9512 <bialix-AT-ukr.net>
9512 <bialix-AT-ukr.net>
9513 \family default
9513 \family default
9514 Improvements for win32 paging system.
9514 Improvements for win32 paging system.
9515 \layout List
9516 \labelwidthstring 00.00.0000
9517
9518 Will\SpecialChar ~
9519 Maier
9520 \family typewriter
9521 <willmaier-AT-ml1.net>
9522 \family default
9523 Official OpenBSD port.
9515 \the_end
9524 \the_end
@@ -89,7 +89,7 b' cd $www'
89
89
90 # Alert package maintainers
90 # Alert package maintainers
91 echo "Alerting package maintainers..."
91 echo "Alerting package maintainers..."
92 maintainers='fperez@colorado.edu ariciputi@users.sourceforge.net jack@xiph.org tretkowski@inittab.de dryice@hotpop.com'
92 maintainers='fperez@colorado.edu ariciputi@users.sourceforge.net jack@xiph.org tretkowski@inittab.de dryice@hotpop.com willmaier@ml1.net'
93 #maintainers='fperez@colorado.edu'
93 #maintainers='fperez@colorado.edu'
94
94
95 for email in $maintainers
95 for email in $maintainers
General Comments 0
You need to be logged in to leave comments. Login now