##// END OF EJS Templates
Removed todo
Removed todo

File last commit:

r4872:34c10438
r11390:e0b789c3
Show More
mkdoctests.py
244 lines | 7.5 KiB | text/x-python | PythonLexer
#!/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.lib import irunner
from IPython.utils.warn 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()