mkdoctests.py
244 lines
| 7.6 KiB
| text/x-python
|
PythonLexer
Brian E Granger
|
r1234 | #!/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 | ||||
Brian Granger
|
r2023 | from IPython.utils.genutils import fatal | ||
Brian E Granger
|
r1234 | |||
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() | ||||