#!/usr/bin/env python """Utility for making a doctest file out of Python or IPython input. %prog [options] input_file [output_file] This script is a convenient generator of doctest files that uses IPython's irunner script to execute valid Python or IPython input in a separate process, capture all of the output, and write it to an output file. It can be used in one of two ways: 1. With a plain Python or IPython input file (denoted by extensions '.py' or '.ipy'. In this case, the output is an auto-generated reST file with a basic header, and the captured Python input and output contained in an indented code block. If no output filename is given, the input name is used, with the extension replaced by '.txt'. 2. With an input template file. Template files are simply plain text files with special directives of the form %run filename to include the named file at that point. If no output filename is given and the input filename is of the form 'base.tpl.txt', the output will be automatically named 'base.txt'. """ # Standard library imports import optparse import os import re import sys import tempfile # IPython-specific libraries from IPython import irunner from IPython.utils.genutils import fatal class IndentOut(object): """A simple output stream that indents all output by a fixed amount. Instances of this class trap output to a given stream and first reformat it to indent every input line.""" def __init__(self,out=sys.stdout,indent=4): """Create an indented writer. :Keywords: - `out` : stream (sys.stdout) Output stream to actually write to after indenting. - `indent` : int Number of spaces to indent every input line by. """ self.indent_text = ' '*indent self.indent = re.compile('^',re.MULTILINE).sub self.out = out self._write = out.write self.buffer = [] self._closed = False def write(self,data): """Write a string to the output stream.""" if self._closed: raise ValueError('I/O operation on closed file') self.buffer.append(data) def flush(self): if self.buffer: data = ''.join(self.buffer) self.buffer[:] = [] self._write(self.indent(self.indent_text,data)) def close(self): self.flush() self._closed = True class RunnerFactory(object): """Code runner factory. This class provides an IPython code runner, but enforces that only one runner is every instantiated. The runner is created based on the extension of the first file to run, and it raises an exception if a runner is later requested for a different extension type. This ensures that we don't generate example files for doctest with a mix of python and ipython syntax. """ def __init__(self,out=sys.stdout): """Instantiate a code runner.""" self.out = out self.runner = None self.runnerClass = None def _makeRunner(self,runnerClass): self.runnerClass = runnerClass self.runner = runnerClass(out=self.out) return self.runner def __call__(self,fname): """Return a runner for the given filename.""" if fname.endswith('.py'): runnerClass = irunner.PythonRunner elif fname.endswith('.ipy'): runnerClass = irunner.IPythonRunner else: raise ValueError('Unknown file type for Runner: %r' % fname) if self.runner is None: return self._makeRunner(runnerClass) else: if runnerClass==self.runnerClass: return self.runner else: e='A runner of type %r can not run file %r' % \ (self.runnerClass,fname) raise ValueError(e) TPL = """ ========================= Auto-generated doctests ========================= This file was auto-generated by IPython in its entirety. If you need finer control over the contents, simply make a manual template. See the mkdoctests.py script for details. %%run %s """ def main(): """Run as a script.""" # Parse options and arguments. parser = optparse.OptionParser(usage=__doc__) newopt = parser.add_option newopt('-f','--force',action='store_true',dest='force',default=False, help='Force overwriting of the output file.') newopt('-s','--stdout',action='store_true',dest='stdout',default=False, help='Use stdout instead of a file for output.') opts,args = parser.parse_args() if len(args) < 1: parser.error("incorrect number of arguments") # Input filename fname = args[0] # We auto-generate the output file based on a trivial template to make it # really easy to create simple doctests. auto_gen_output = False try: outfname = args[1] except IndexError: outfname = None if fname.endswith('.tpl.txt') and outfname is None: outfname = fname.replace('.tpl.txt','.txt') else: bname, ext = os.path.splitext(fname) if ext in ['.py','.ipy']: auto_gen_output = True if outfname is None: outfname = bname+'.txt' # Open input file # In auto-gen mode, we actually change the name of the input file to be our # auto-generated template if auto_gen_output: infile = tempfile.TemporaryFile() infile.write(TPL % fname) infile.flush() infile.seek(0) else: infile = open(fname) # Now open the output file. If opts.stdout was given, this overrides any # explicit choice of output filename and just directs all output to # stdout. if opts.stdout: outfile = sys.stdout else: # Argument processing finished, start main code if os.path.isfile(outfname) and not opts.force: fatal("Output file %r exists, use --force (-f) to overwrite." % outfname) outfile = open(outfname,'w') # all output from included files will be indented indentOut = IndentOut(outfile,4) getRunner = RunnerFactory(indentOut) # Marker in reST for transition lines rst_transition = '\n'+'-'*76+'\n\n' # local shorthand for loop write = outfile.write # Process input, simply writing back out all normal lines and executing the # files in lines marked as '%run filename'. for line in infile: if line.startswith('%run '): # We don't support files with spaces in their names. incfname = line.split()[1] # We make the output of the included file appear bracketed between # clear reST transition marks, and indent it so that if anyone # makes an HTML or PDF out of the file, all doctest input and # output appears in proper literal blocks. write(rst_transition) write('Begin included file %s::\n\n' % incfname) # I deliberately do NOT trap any exceptions here, so that if # there's any problem, the user running this at the command line # finds out immediately by the code blowing up, rather than ending # up silently with an incomplete or incorrect file. getRunner(incfname).run_file(incfname) write('\nEnd included file %s\n' % incfname) write(rst_transition) else: # The rest of the input file is just written out write(line) infile.close() # Don't close sys.stdout!!! if outfile is not sys.stdout: outfile.close() if __name__ == '__main__': main()