Welcome, guest | Sign In | My Account | Store | Cart

The doctester extracts code from stdin and tests it using the doctest module in the standard library. It can be invoked from the command line, but it is best called from you editor of choice. I just give an example for Emacs.

Python, 49 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/usr/bin/env python
# Author: michele.simionato@gmail.com
"""\
Filter passing stdin through doctest. Example of usage:
$ doctester.py -v < file.txt
"""
import sys, doctest, textwrap, re, types

# regular expressions to identify code blocks of the form
#<scriptname.py> ... </scriptname.py>
DOTNAME = r'\b[a-zA-Z_][\w\.]*', # identifier with or without dots
SCRIPT = re.compile(r'(?s)#<(%s)>(.*?)#</\1>' % DOTNAME)

# a simple utility to extract the scripts contained in the original text
def scripts(txt):
    for MO in SCRIPT.finditer(txt):
        yield MO.group(1), textwrap.dedent(MO.group(2))

# save the scripts in the current directory
def savescripts(txt):
    scriptdict = {}
    for scriptname, script in scripts(txt): # read scripts
        if scriptname not in scriptdict:
            scriptdict[scriptname] = script
        else:
            scriptdict[scriptname] += script
    for scriptname in scriptdict: # save scripts
        code = '# ' + scriptname + scriptdict[scriptname]
        print >> file(scriptname, 'w'), code

# based on a clever trick: it converts the original text into the docstring of
# a dynamically generated module; works both for Python 2.3 and 2.4
def runtests(txt, verbose=False):
    savescripts(txt)
    dynmod = types.ModuleType('<current-buffer>', txt)   
    failed, tot = doctest.testmod(dynmod, verbose=verbose)
    if not verbose:
        print >> sys.stderr, "doctest: %s failed of %s" % (failed, tot)
    return failed, tot

if __name__=='__main__':
    try: set # need sets for option parsing
    except NameError: import sets; set = sets.Set # for Python 2.3
    valid_options = set("-v -h".split())
    options = set(sys.argv[1:])
    assert options < valid_options, "Unrecognized option"
    if "-h" in options: # print usage message and exit
        sys.exit(__doc__)
    runtests(sys.stdin.read(), "-v" in options)

I use this little script a lot: to test my posts to c.l.py, to test my articles, and to test my libraries. Since the script is extremely simple and useful, I thought I will share it.

The first thing you need is a text like this, with cut and pasted interpreter sessions:

>>> 1 + 1
2

The doctester will look for snippets of this form and will test them. The magic is performed by the doctest module in the standard library. I have just added the possibity of inserting named modules in the text file, like this one::

#\<example_module.py\>

a = 1

#\

The doctester will extract code like this and save it in your current directory, under the name example_module.py, before running the tests. In this way you can import the module in your tests:

>>> from example_module import a
>>> print a
1

You may define any number of modules in the same way. You can also add code to a previously defined module, simply by repeating the module name::

#\<example_module.py\>

b = 2

#\

>>> from example_module import b
>>> print b
2

Ideally, in future extensions, it will be possible to insert snippets of code in other languages (for instance bash).

The doctester can be used from the command line or called from an external program. For instance, you could pass to the doctester this text you are reading now: suppose it is stored in a file called doctester.txt, you will get

::

$ python doctester.py doctest: 0 failed of 5

or, if you prefer a more explicit output,

::

$ python doctester.py -v < doctester.txt Trying: 1 + 1 Expecting: 2 ok Trying: from example_module import a Expecting nothing ok Trying: print a Expecting: 1 ok Trying: from example_module import b Expecting nothing ok Trying: print b Expecting: 2 ok 1 items passed all tests: 5 tests in <current-buffer> 5 tests in 1 items. 5 passed and 0 failed. Test passed.

The message says that the tests were defined in '<current-buffer>': the reason is that I usually call the doctester from Emacs, when I am editing the text. If you have Python 2.4 and the doctester in installed in your current Python path, you can just put the following in your .emacs::

;; passing the current buffer to an external tool (defun run-doctest () (interactive) (shell-command-on-region (beginning-of-buffer) (end-of-buffer) "python2.4 -m doctester" current-prefix-arg current-prefix-arg))

(defun run-doctest-verbose () (interactive) (shell-command-on-region (beginning-of-buffer) (end-of-buffer) "python2.4 -m doctester -v" current-prefix-arg current-prefix-arg))

;; F6 for regular output, SHIFT-F6 for verbose output (global-set-key [f6] 'run-doctest) (global-set-key [(shift f6)] 'run-doctest-verbose)

If you have Python 2.3 you may have to work a bit more, or may just insert the full pathname of the doctester script. Obviously you may change the keybindings to whatever you like. I am pretty sure you can invoke the doctester from the Other Editor (TM) too ;)

P.S. I have tried everything to get angular brackets displayed properly in the site, but < and > do not work; so I put a backslash but you should not use it. Damn cookbook site!