Add files via upload
This commit is contained in:
parent
d5cd1ed8e1
commit
3b06853ded
238
docex.py
Normal file
238
docex.py
Normal file
@ -0,0 +1,238 @@
|
||||
"""A framework for running unit tests and examples, written in docstrings.
|
||||
|
||||
This lets you write "Ex: sqrt(4) ==> 2; sqrt(-1) raises ValueError" in a
|
||||
docstring, and then execute the examples as unit tests.
|
||||
|
||||
This functionality is similar to the doctest module. The major
|
||||
differences between docex and doctest are:
|
||||
|
||||
(1) Brevity. With docex you write the one-line comment
|
||||
"Ex: len('abc') ==> 3; len([]) ==> 0; len(5) raises TypeError"
|
||||
With doctest you would need 9 lines for the same thing:
|
||||
'''>>> len('abc')
|
||||
3
|
||||
>>> len([])
|
||||
0
|
||||
>>> len(5))
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: len() of unsized object
|
||||
'''
|
||||
|
||||
(2) Docex handles both examples and unit tests.
|
||||
It took me a while to recognize this distinction: when I write
|
||||
"sqrt(4) ==> 2" it has two purposes -- to serve as a unit test
|
||||
and to serve as an example of how to use the sqrt function.
|
||||
When I write "random.choice('abc')" it serves as an example of
|
||||
how to use the choice function, but it is not a unit test.
|
||||
docex lets you do both; doctest only supports tests. Of course
|
||||
you can coerce this into a test in doctest, with something like
|
||||
>>> random.choice('abc') in 'abc'
|
||||
True
|
||||
|
||||
(3) Eval-based rather than string-comparison based. The docex string
|
||||
"dict(zip([1,4,9], [1,2,3])) ==> {1: 1, 4: 2, 9: 3}" works even
|
||||
when a different version of Python decides to print the dict as
|
||||
"{9: 3, 4: 2, 1: 1}" because docex evals the right-hand-side and
|
||||
checks to see if it is equal. That's good for dicts, its good for
|
||||
writing "1+1==2 ==> True" and having it work in versions of Python
|
||||
where True prints as "1" rather than as "True", and so on,
|
||||
but doctest has the edge if you want to compare against something
|
||||
that doesn't have an eval-able output, or if you want to test
|
||||
printed output.
|
||||
|
||||
(4) Doctest has many more features, and is better supported.
|
||||
I wrote docex before doctest was an official part of Python, but
|
||||
with the refactoring of doctest in Python 2.4, I decided to switch
|
||||
my code over to doctest, even though I prefer the brevity of docex.
|
||||
I still offer docex for those who want it.
|
||||
|
||||
From Python, when you want to test modules m1, m2, ... do:
|
||||
docex.Docex([m1, m2, ...])
|
||||
From the shell, when you want to test files *.py, do:
|
||||
python docex.py [log-file] *.py
|
||||
If log file ends in .htm or .html, it will be written in HTML.
|
||||
If log file is -, or if it is missing, then standard output is used.
|
||||
|
||||
For each module, Docex looks at the __doc__ and _docex strings of the
|
||||
module itself, and of each member, and recursively for each member
|
||||
class. If a line in a docstring starts with r'^\s*Ex: ' (a line with
|
||||
blanks, then 'Ex: '), then the remainder of the string after the colon
|
||||
is treated as examples. Each line of the examples should conform to
|
||||
one of the following formats:
|
||||
|
||||
(1) Blank line or a comment; these just get echoed verbatim to the log.
|
||||
(2) Of the form example1 ; example2 ; ...
|
||||
(3) Of the form 'x ==> y' for any expressions x and y.
|
||||
x is evaled and assigned to _, then y is evaled.
|
||||
If x != y, an error message is printed.
|
||||
(4) Of the form 'x raises y', for any statement x and expression y.
|
||||
First y is evaled to yield an exception type, then x is execed.
|
||||
If x doesn't raise the right exception, an error msg is printed.
|
||||
(5) Of the form 'statement'. Statement is execed for side effect.
|
||||
(6) Of the form 'expression'. Expression is evaled for side effect.
|
||||
"""
|
||||
|
||||
import re, sys, types
|
||||
|
||||
class Docex:
|
||||
"""A class to run test examples written in docstrings or in _docex."""
|
||||
|
||||
def __init__(self, modules=None, html=0, out=None,
|
||||
title='Docex Example Output'):
|
||||
if modules is None:
|
||||
modules = sys.modules.values()
|
||||
self.passed = self.failed = 0;
|
||||
self.dictionary = {}
|
||||
self.already_seen = {}
|
||||
self.html = html
|
||||
try:
|
||||
if out: sys.stdout = out
|
||||
self.writeln(title, '<h1>', '</h1><pre>')
|
||||
for module in modules:
|
||||
self.run_module(module)
|
||||
self.writeln(str(self), '</pre>\n<hr><h1>', '</h1>\n')
|
||||
finally:
|
||||
if out:
|
||||
sys.stdout = sys.__stdout__
|
||||
out.close()
|
||||
|
||||
def __repr__(self):
|
||||
if self.failed:
|
||||
return ('<Test: #### failed %d, passed %d>'
|
||||
% (self.failed, self.passed))
|
||||
else:
|
||||
return '<Test: passed all %d>' % self.passed
|
||||
|
||||
def run_module(self, object):
|
||||
"""Run the docstrings, and then all members of the module."""
|
||||
if not self.seen(object):
|
||||
self.dictionary.update(vars(object)) # import module into self
|
||||
name = object.__name__
|
||||
self.writeln('## Module %s ' % name,
|
||||
'\n</pre><a name=%s><h1>' % name,
|
||||
'</h1><pre>')
|
||||
self.run_docstring(object)
|
||||
names = object.__dict__.keys()
|
||||
names.sort()
|
||||
for name in names:
|
||||
val = object.__dict__[name]
|
||||
if isinstance(val, types.ClassType):
|
||||
self.run_class(val)
|
||||
elif isinstance(val, types.ModuleType):
|
||||
pass
|
||||
elif not self.seen(val):
|
||||
self.run_docstring(val)
|
||||
|
||||
def run_class(self, object):
|
||||
"""Run the docstrings, and then all members of the class."""
|
||||
if not self.seen(object):
|
||||
self.run_docstring(object)
|
||||
names = object.__dict__.keys()
|
||||
names.sort()
|
||||
for name in names:
|
||||
self.run_docstring(object.__dict__[name])
|
||||
|
||||
def run_docstring(self, object, search=re.compile(r'(?m)^\s*Ex: ').search):
|
||||
"Run the __doc__ and _docex attributes, if the object has them."
|
||||
if hasattr(object, '__doc__'):
|
||||
s = object.__doc__
|
||||
if isinstance(s, str):
|
||||
match = search(s)
|
||||
if match: self.run_string(s[match.end():])
|
||||
if hasattr(object, '_docex'):
|
||||
self.run_string(object._docex)
|
||||
|
||||
def run_string(self, teststr):
|
||||
"""Run a test string, printing inputs and results."""
|
||||
if not teststr: return
|
||||
teststr = teststr.strip()
|
||||
if teststr.find('\n') > -1:
|
||||
map(self.run_string, teststr.split('\n'))
|
||||
elif teststr == '' or teststr.startswith('#'):
|
||||
self.writeln(teststr)
|
||||
elif teststr.find('; ') > -1:
|
||||
for substr in teststr.split('; '): self.run_string(substr)
|
||||
elif teststr.find('==>') > -1:
|
||||
teststr, result = teststr.split('==>')
|
||||
self.evaluate(teststr, result)
|
||||
elif teststr.find(' raises ') > -1:
|
||||
teststr, exception = teststr.split(' raises ')
|
||||
self.raises(teststr, exception)
|
||||
else: ## Try to eval, but if it is a statement, exec
|
||||
try:
|
||||
self.evaluate(teststr)
|
||||
except SyntaxError:
|
||||
exec teststr in self.dictionary
|
||||
|
||||
def evaluate(self, teststr, resultstr=None):
|
||||
"Eval teststr and check if resultstr (if given) evals to the same."
|
||||
self.writeln('>>> ' + teststr.strip())
|
||||
result = eval(teststr, self.dictionary)
|
||||
self.dictionary['_'] = result
|
||||
self.writeln(repr(result))
|
||||
if resultstr == None:
|
||||
return
|
||||
elif result == eval(resultstr, self.dictionary):
|
||||
self.passed += 1
|
||||
else:
|
||||
self.fail(teststr, resultstr)
|
||||
|
||||
def raises(self, teststr, exceptionstr):
|
||||
teststr = teststr.strip()
|
||||
self.writeln('>>> ' + teststr)
|
||||
except_class = eval(exceptionstr, self.dictionary)
|
||||
try:
|
||||
exec teststr in self.dictionary
|
||||
except except_class:
|
||||
self.writeln('# raises %s as expected' % exceptionstr)
|
||||
self.passed += 1
|
||||
return
|
||||
self.fail(teststr, exceptionstr)
|
||||
|
||||
def fail(self, teststr, resultstr):
|
||||
self.writeln('###### ERROR, TEST FAILED: expected %s for %s'
|
||||
% (resultstr, teststr),
|
||||
'<font color=red><b>', '</b></font>')
|
||||
self.failed += 1
|
||||
|
||||
def writeln(self, s, before='', after=''):
|
||||
"Write s, html escaped, and wrapped with html code before and after."
|
||||
s = str(s)
|
||||
if self.html:
|
||||
s = s.replace('&','&').replace('<','<').replace('>','>')
|
||||
print '%s%s%s' % (before, s, after)
|
||||
else:
|
||||
print s
|
||||
|
||||
def seen(self, object):
|
||||
"""Return true if this object has been seen before.
|
||||
In any case, record that we have seen it."""
|
||||
result = self.already_seen.has_key(id(object))
|
||||
self.already_seen[id(object)] = 1
|
||||
return result
|
||||
|
||||
def main(args):
|
||||
"""Run Docex. args should be a list of python filenames.
|
||||
If the first arg is a non-python filename, it is taken as the
|
||||
name of a log file to which output is written. If it ends in
|
||||
".htm" or ".html", then the output is written as html. If the
|
||||
first arg is "-", then standard output is used as the log file."""
|
||||
import glob
|
||||
out = None
|
||||
html = 0
|
||||
if args[0] != "-" and not args[0].endswith(".py"):
|
||||
out = open(args[0], 'w')
|
||||
if args[0].endswith(".html") or args[0].endswith(".htm"):
|
||||
html = 1
|
||||
modules = []
|
||||
for arg in args:
|
||||
for file in glob.glob(arg):
|
||||
if file.endswith('.py'):
|
||||
modules.append(__import__(file[:-3]))
|
||||
print Docex(modules, html=html, out=out)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user