From fc07f98bff18501d6406bbba2e29f3eb4bfa31b5 2007-01-27 07:30:22 From: fperez Date: 2007-01-27 07:30:22 Subject: [PATCH] - Bug fixes in Demo code to support demos with IPython syntax - Extensive irunner changes needed for doctesting out of process IPython examples. --- diff --git a/IPython/Magic.py b/IPython/Magic.py index 4c8fc49..d8ad53f 100644 --- a/IPython/Magic.py +++ b/IPython/Magic.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Magic functions for InteractiveShell. -$Id: Magic.py 1981 2006-12-12 21:51:54Z vivainio $""" +$Id: Magic.py 2036 2007-01-27 07:30:22Z fperez $""" #***************************************************************************** # Copyright (C) 2001 Janko Hauser and @@ -2617,16 +2617,20 @@ Defaulting color scheme to 'NoColor'""" if ps: try: os.chdir(os.path.expanduser(ps)) - ttitle = ("IPy:" + ( - os.getcwd() == '/' and '/' or os.path.basename(os.getcwd()))) - platutils.set_term_title(ttitle) + if self.shell.rc.term_title: + #print 'set term title:',self.shell.rc.term_title # dbg + ttitle = ("IPy:" + ( + os.getcwd() == '/' and '/' or \ + os.path.basename(os.getcwd()))) + platutils.set_term_title(ttitle) except OSError: print sys.exc_info()[1] else: self.shell.user_ns['_dh'].append(os.getcwd()) else: os.chdir(self.shell.home_dir) - platutils.set_term_title("IPy:~") + if self.shell.rc.term_title: + platutils.set_term_title("IPy:~") self.shell.user_ns['_dh'].append(os.getcwd()) if not 'q' in opts: print self.shell.user_ns['_dh'][-1] diff --git a/IPython/demo.py b/IPython/demo.py index 35c65eb..91d0bae 100644 --- a/IPython/demo.py +++ b/IPython/demo.py @@ -5,6 +5,10 @@ in IPython for demonstrations. With very simple markup (a few tags in comments), you can control points where the script stops executing and returns control to IPython. + +Provided classes +================ + The classes are (see their docstrings for further details): - Demo: pure python demos @@ -20,6 +24,24 @@ The classes are (see their docstrings for further details): executed a line at a time, but processed via IPython). +Subclassing +=========== + +The classes here all include a few methods meant to make customization by +subclassing more convenient. Their docstrings below have some more details: + + - marquee(): generates a marquee to provide visible on-screen markers at each + block start and end. + + - pre_cmd(): run right before the execution of each block. + + - pre_cmd(): run right after the execution of each block. If the block + raises an exception, this is NOT called. + + +Operation +========= + The file is run in its own empty namespace (though you can pass it a string of arguments as if in a command line environment, and it will see those as sys.argv). But at each stop, the global IPython namespace is updated with the @@ -76,6 +98,12 @@ has a few useful methods for navigation, like again(), edit(), jump(), seek() and back(). It can be reset for a new run via reset() or reloaded from disk (in case you've edited the source) via reload(). See their docstrings below. + +Example +======= + +The following is a very simple example of a valid demo file. + #################### EXAMPLE DEMO ############################### '''A simple interactive demo to illustrate the use of IPython's Demo class.''' @@ -111,6 +139,7 @@ print 'z is now:', z print 'bye!' ################### END EXAMPLE DEMO ############################ """ + #***************************************************************************** # Copyright (C) 2005-2006 Fernando Perez. # @@ -296,8 +325,8 @@ class Demo: if index is None: return - print marquee('<%s> block # %s (%s remaining)' % - (self.fname,index,self.nblocks-index-1)) + print self.marquee('<%s> block # %s (%s remaining)' % + (self.fname,index,self.nblocks-index-1)) print self.src_blocks_colored[index], sys.stdout.flush() @@ -307,6 +336,7 @@ class Demo: fname = self.fname nblocks = self.nblocks silent = self._silent + marquee = self.marquee for index,block in enumerate(self.src_blocks_colored): if silent[index]: print marquee('<%s> SILENT block # %s (%s remaining)' % @@ -335,6 +365,7 @@ class Demo: if index is None: return try: + marquee = self.marquee next_block = self.src_blocks[index] self.block_index += 1 if self._silent[index]: @@ -353,7 +384,9 @@ class Demo: try: save_argv = sys.argv sys.argv = self.sys_argv + self.pre_cmd() self.runlines(next_block) + self.post_cmd() finally: sys.argv = save_argv @@ -364,10 +397,25 @@ class Demo: if self.block_index == self.nblocks: print - print marquee(' END OF DEMO ') - print marquee('Use reset() if you want to rerun it.') + print self.marquee(' END OF DEMO ') + print self.marquee('Use reset() if you want to rerun it.') self.finished = True + # These methods are meant to be overridden by subclasses who may wish to + # customize the behavior of of their demos. + def marquee(self,txt='',width=78,mark='*'): + """Return the input string centered in a 'marquee'.""" + return marquee(txt,width,mark) + + def pre_cmd(self): + """Method called before executing each block.""" + pass + + def post_cmd(self): + """Method called after executing each block.""" + pass + + class IPythonDemo(Demo): """Class for interactive demos with IPython's input processing applied. @@ -384,7 +432,7 @@ class IPythonDemo(Demo): def runlines(self,source): """Execute a string with one or more lines of code""" - self.runlines(source) + self.shell.runlines(source) class LineDemo(Demo): """Demo where each line is executed as a separate block. diff --git a/IPython/ipmaker.py b/IPython/ipmaker.py index 1713fea..77825bc 100644 --- a/IPython/ipmaker.py +++ b/IPython/ipmaker.py @@ -6,7 +6,7 @@ Requires Python 2.1 or better. This file contains the main make_IPython() starter function. -$Id: ipmaker.py 2029 2007-01-22 06:35:15Z fperez $""" +$Id: ipmaker.py 2036 2007-01-27 07:30:22Z fperez $""" #***************************************************************************** # Copyright (C) 2001-2006 Fernando Perez. @@ -162,7 +162,7 @@ object? -> Details about 'object'. ?object also works, ?? prints more. 'separate_out2|so2=s xmode=s wildcards_case_sensitive! ' 'magic_docstrings system_verbose! ' 'multi_line_specials! ' - 'wxversion=s ' + 'term_title! wxversion=s ' 'autoedit_syntax!') # Options that can *only* appear at the cmd line (not in rcfiles). @@ -225,6 +225,7 @@ object? -> Details about 'object'. ?object also works, ?? prints more. q4thread = 0, wthread = 0, pylab = 0, + term_title = 1, tk = 0, upgrade = 0, Version = 0, diff --git a/IPython/irunner.py b/IPython/irunner.py index 6a9d64b..077dc26 100755 --- a/IPython/irunner.py +++ b/IPython/irunner.py @@ -49,7 +49,7 @@ runner [opts] script_name class InteractiveRunner(object): """Class to run a sequence of commands through an interactive program.""" - def __init__(self,program,prompts,args=None): + def __init__(self,program,prompts,args=None,out=sys.stdout,echo=True): """Construct a runner. Inputs: @@ -73,6 +73,9 @@ class InteractiveRunner(object): - args(None): optional list of strings to pass as arguments to the child program. + - out(sys.stdout): if given, an output stream to be used when writing + output. The only requirement is that it must have a .write() method. + Public members not parameterized in the constructor: - delaybeforesend(0): Newer versions of pexpect have a delay before @@ -87,11 +90,30 @@ class InteractiveRunner(object): self.prompts = prompts if args is None: args = [] self.args = args + self.out = out + self.echo = echo # Other public members which we don't make as parameters, but which # users may occasionally want to tweak self.delaybeforesend = 0 - - def run_file(self,fname,interact=False): + + # Create child process and hold on to it so we don't have to re-create + # for every single execution call + c = self.child = pexpect.spawn(self.program,self.args,timeout=None) + c.delaybeforesend = self.delaybeforesend + # pexpect hard-codes the terminal size as (24,80) (rows,columns). + # This causes problems because any line longer than 80 characters gets + # completely overwrapped on the printed outptut (even though + # internally the code runs fine). We reset this to 99 rows X 200 + # columns (arbitrarily chosen), which should avoid problems in all + # reasonable cases. + c.setwinsize(99,200) + + def close(self): + """close child process""" + + self.child.close() + + def run_file(self,fname,interact=False,get_output=False): """Run the given file interactively. Inputs: @@ -103,11 +125,13 @@ class InteractiveRunner(object): fobj = open(fname,'r') try: - self.run_source(fobj,interact) + out = self.run_source(fobj,interact,get_output) finally: fobj.close() + if get_output: + return out - def run_source(self,source,interact=False): + def run_source(self,source,interact=False,get_output=False): """Run the given source code interactively. Inputs: @@ -119,6 +143,12 @@ class InteractiveRunner(object): - interact(False): if true, start to interact with the running program at the end of the script. Otherwise, just exit. + + - get_output(False): if true, capture the output of the child process + (filtering the input commands out) and return it as a string. + + Returns: + A string containing the process output, but only if requested. """ # if the source is a string, chop it up in lines so we can iterate @@ -126,39 +156,38 @@ class InteractiveRunner(object): if not isinstance(source,file): source = source.splitlines(True) - # grab the true write method of stdout, in case anything later - # reassigns sys.stdout, so that we really are writing to the true - # stdout and not to something else. We also normalize all strings we - # write to use the native OS line separators. - linesep = os.linesep - stdwrite = sys.stdout.write - write = lambda s: stdwrite(s.replace('\r\n',linesep)) - - c = pexpect.spawn(self.program,self.args,timeout=None) - c.delaybeforesend = self.delaybeforesend - - # pexpect hard-codes the terminal size as (24,80) (rows,columns). - # This causes problems because any line longer than 80 characters gets - # completely overwrapped on the printed outptut (even though - # internally the code runs fine). We reset this to 99 rows X 200 - # columns (arbitrarily chosen), which should avoid problems in all - # reasonable cases. - c.setwinsize(99,200) + if self.echo: + # normalize all strings we write to use the native OS line + # separators. + linesep = os.linesep + stdwrite = self.out.write + write = lambda s: stdwrite(s.replace('\r\n',linesep)) + else: + # Quiet mode, all writes are no-ops + write = lambda s: None + c = self.child prompts = c.compile_pattern_list(self.prompts) - prompt_idx = c.expect_list(prompts) + # Flag whether the script ends normally or not, to know whether we can # do anything further with the underlying process. end_normal = True + + # If the output was requested, store it in a list for return at the end + if get_output: + output = [] + store_output = output.append + for cmd in source: # skip blank lines for all matches to the 'main' prompt, while the # secondary prompts do not if prompt_idx==0 and \ (cmd.isspace() or cmd.lstrip().startswith('#')): - print cmd, + write(cmd) continue + #write('AFTER: '+c.after) # dbg write(c.after) c.send(cmd) try: @@ -168,8 +197,17 @@ class InteractiveRunner(object): write(c.before) end_normal = False break + write(c.before) - + + # With an echoing process, the output we get in c.before contains + # the command sent, a newline, and then the actual process output + if get_output: + store_output(c.before[len(cmd+'\n'):]) + #write('CMD: <<%s>>' % cmd) # dbg + #write('OUTPUT: <<%s>>' % output[-1]) # dbg + + self.out.flush() if end_normal: if interact: c.send('\n') @@ -182,13 +220,19 @@ class InteractiveRunner(object): # space is there to make sure it gets printed, otherwise # OS buffering sometimes just suppresses it. write(' \n') - sys.stdout.flush() - else: - c.close() + self.out.flush() else: if interact: e="Further interaction is not possible: child process is dead." print >> sys.stderr, e + + # Leave the child ready for more input later on, otherwise select just + # hangs on the second invocation. + c.send('\n') + + # Return any requested output + if get_output: + return ''.join(output) def main(self,argv=None): """Run as a command-line script.""" @@ -221,26 +265,28 @@ class IPythonRunner(InteractiveRunner): prompts would break this. """ - def __init__(self,program = 'ipython',args=None): + def __init__(self,program = 'ipython',args=None,out=sys.stdout,echo=True): """New runner, optionally passing the ipython command to use.""" args0 = ['-colors','NoColor', '-pi1','In [\\#]: ', - '-pi2',' .\\D.: '] + '-pi2',' .\\D.: ', + '-noterm_title', + '-noautoindent'] if args is None: args = args0 else: args = args0 + args prompts = [r'In \[\d+\]: ',r' \.*: '] - InteractiveRunner.__init__(self,program,prompts,args) + InteractiveRunner.__init__(self,program,prompts,args,out,echo) class PythonRunner(InteractiveRunner): """Interactive Python runner.""" - def __init__(self,program='python',args=None): + def __init__(self,program='python',args=None,out=sys.stdout,echo=True): """New runner, optionally passing the python command to use.""" prompts = [r'>>> ',r'\.\.\. '] - InteractiveRunner.__init__(self,program,prompts,args) + InteractiveRunner.__init__(self,program,prompts,args,out,echo) class SAGERunner(InteractiveRunner): @@ -250,11 +296,11 @@ class SAGERunner(InteractiveRunner): to use 'colors NoColor' in the ipythonrc config file, since currently the prompt matching regexp does not identify color sequences.""" - def __init__(self,program='sage',args=None): + def __init__(self,program='sage',args=None,out=sys.stdout,echo=True): """New runner, optionally passing the sage command to use.""" prompts = ['sage: ',r'\s*\.\.\. '] - InteractiveRunner.__init__(self,program,prompts,args) + InteractiveRunner.__init__(self,program,prompts,args,out,echo) # Global usage string, to avoid indentation issues if typed in a function def. MAIN_USAGE = """ diff --git a/doc/ChangeLog b/doc/ChangeLog index 7562771..78ab8ba 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,3 +1,15 @@ +2007-01-27 Fernando Perez + + * IPython/irunner.py (InteractiveRunner.run_source): major updates + to irunner to allow it to correctly support real doctesting of + out-of-process ipython code. + + * IPython/Magic.py (magic_cd): Make the setting of the terminal + title an option (-noterm_title) because it completely breaks + doctesting. + + * IPython/demo.py: fix IPythonDemo class that was not actually working. + 2007-01-24 Fernando Perez * IPython/irunner.py (main): fix small bug where extensions were