[Checkins] SVN: manuel/trunk/src/manuel/ - add a larger example of using Manuel (table-example.txt)
Benji York
benji at zope.com
Sat Jun 6 13:00:26 EDT 2009
Log message for revision 100664:
- add a larger example of using Manuel (table-example.txt)
- make the test suite factory function try harder to find the calling
module
- fix a bug in the order regions are evaluated
- add a Manuel object that can evaluate Python code in
".. code-block:: python" regions of a reST document
Changed:
U manuel/trunk/src/manuel/__init__.py
A manuel/trunk/src/manuel/codeblock.py
A manuel/trunk/src/manuel/table-example.txt
U manuel/trunk/src/manuel/testing.py
U manuel/trunk/src/manuel/tests.py
-=-
Modified: manuel/trunk/src/manuel/__init__.py
===================================================================
--- manuel/trunk/src/manuel/__init__.py 2009-06-06 13:39:45 UTC (rev 100663)
+++ manuel/trunk/src/manuel/__init__.py 2009-06-06 17:00:25 UTC (rev 100664)
@@ -255,8 +255,8 @@
def evaluate_with(self, m, globs):
globs = GlobWrapper(globs)
- for evaluater in sort_handlers(m.evaluaters):
- for region in list(self):
+ for region in list(self):
+ for evaluater in sort_handlers(m.evaluaters):
evaluater(region, self, globs)
def format_with(self, m):
Added: manuel/trunk/src/manuel/codeblock.py
===================================================================
--- manuel/trunk/src/manuel/codeblock.py (rev 0)
+++ manuel/trunk/src/manuel/codeblock.py 2009-06-06 17:00:25 UTC (rev 100664)
@@ -0,0 +1,36 @@
+import re
+import manuel
+import textwrap
+
+CODEBLOCK_START = re.compile(
+ r'^\.\.\s*code-block::\s*python\s*$', re.MULTILINE)
+
+# XXX should probably take end-of-file into account
+CODEBLOCK_END = re.compile(r'^\S.*$', re.MULTILINE)
+
+
+class CodeBlock(object):
+ def __init__(self, code):
+ self.code = code
+
+
+def find_code_blocks(document):
+ for region in document.find_regions(CODEBLOCK_START, CODEBLOCK_END):
+ source = textwrap.dedent('\n'.join(region.source.splitlines()[1:-1]))
+ source_location = '%s:%d' % (document.location, region.lineno)
+ code = compile(source, source_location, 'exec', 0, True)
+ document.replace_region(region, CodeBlock(code))
+
+
+def execute_code_blocks(region, document, globs):
+ if not isinstance(region.parsed, CodeBlock):
+ return
+
+ code = region.parsed.code
+ exec code in globs
+ del globs['__builtins__'] # exec adds __builtins__, we don't want it
+
+
+class Manuel(manuel.Manuel):
+ def __init__(self):
+ manuel.Manuel.__init__(self, [find_code_blocks], [execute_code_blocks])
Property changes on: manuel/trunk/src/manuel/codeblock.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: manuel/trunk/src/manuel/table-example.txt
===================================================================
--- manuel/trunk/src/manuel/table-example.txt (rev 0)
+++ manuel/trunk/src/manuel/table-example.txt 2009-06-06 17:00:25 UTC (rev 100664)
@@ -0,0 +1,280 @@
+FIT Table Example
+=================
+
+Occasionally when structuring a doctest, you want to succinctly express several
+sets of inputs and outputs of a particular function.
+
+That's something `FIT <http://fit.c2.com/wiki.cgi?SimpleExample>`_ tables do a
+good job of.
+
+Lets write a simple table evaluator using Manuel.
+
+We'll use `reST <http://docutils.sourceforge.net/rst.html>' tables. The table
+source will look like this:
+
+ ===== ===== ======
+ \ A or B
+ --------------------
+ A B Result
+ ===== ===== ======
+ False False False
+ True False True
+ False True True
+ True True True
+ ===== ===== ======
+
+When rendered, it will look like this:
+
+===== ===== ======
+\ A or B
+--------------------
+ A B Result
+===== ===== ======
+False False False
+True False True
+False True True
+True True True
+===== ===== ======
+
+Lets imagine that the source of our test was stored in a string:
+
+ >>> source = """\
+ ... The "or" operator
+ ... =================
+ ...
+ ... Here is an example of the "or" operator in action:
+ ...
+ ... ===== ===== ======
+ ... \ A or B
+ ... --------------------
+ ... A B Result
+ ... ===== ===== ======
+ ... False False False
+ ... True False True
+ ... False True True
+ ... True True True
+ ... ===== ===== ======
+ ... """
+
+
+Parsing
+-------
+
+First we need a function to find the tables::
+
+.. code-block:: python
+
+ import re
+
+ table_start = re.compile(r'(?<=\n\n)=[= ]+\n(?=[ \t]*?\S)', re.DOTALL)
+ table_end = re.compile(r'\n=[= ]+\n(?=\Z|\n)', re.DOTALL)
+
+ class Table(object):
+ def __init__(self, source):
+ self.source = source
+
+ def find_tables(document):
+ for region in document.find_regions(table_start, table_end):
+ table = Table(region.source)
+ document.replace_region(region, table)
+
+Instances of the class "Table" will represent the tables.
+
+Now we can create a Manuel Document from the source and use the "find_tables"
+function on it.
+
+ >>> import manuel
+ >>> document = manuel.Document(source, location='fake.txt')
+ >>> find_tables(document)
+
+If we examine the Docuement object we can see that the table was recognized.
+
+ >>> region = list(document)[1]
+ >>> print region.source,
+ ===== ===== ======
+ \ A or B
+ --------------------
+ A B Result
+ ===== ===== ======
+ False False False
+ True False True
+ False True True
+ True True True
+ ===== ===== ======
+
+ >>> region.parsed
+ <Table object at ...>
+
+
+Evaluating
+==========
+
+Now that we can find and extract the tables from the source, we need to be able
+to evaluate them.
+
+.. code-block:: python
+
+ class TableErrors(list):
+ pass
+
+
+ class TableError(object):
+ def __init__(self, location, lineno, expected, got):
+ self.location = location
+ self.lineno = lineno
+ self.expected = expected
+ self.got = got
+
+ def __str__(self):
+ return '<%s %s:%s>' % (
+ self.__class__.__name__, self.location, self.lineno)
+
+
+ def evaluate_table(region, document, globs):
+ if not isinstance(region.parsed, Table):
+ return
+
+ lines = enumerate(iter(region.source.splitlines()))
+ lines.next() # skip the first line
+ expression = lines.next()[1]
+ lines.next() # skip the divider line
+ variables = [v.strip() for v in lines.next()[1].split()][:-1]
+
+ errors = TableErrors()
+
+ lines.next() # skip the divider line
+ for lineno_offset, line in lines:
+ if line.startswith('='):
+ break # we ran into the final divider, so stop
+
+ values = [eval(v.strip(), {}) for v in line.split()]
+ inputs = values[:-1]
+ output = values[-1]
+
+ if expression.startswith('\\'):
+ expression = expression[1:]
+ result = eval(expression, dict(zip(variables, inputs)))
+ if result != output:
+ lineno = region.lineno + lineno_offset
+ errors.append(
+ TableError(document.location, lineno, output, result))
+
+ region.evaluated = errors
+
+Now the table can be evaluated:
+
+ >>> evaluate_table(region, document, {})
+
+Yay! There were no errors:
+
+ >>> region.evaluated
+ []
+
+What would happen if there were?
+
+ >>> source_with_errors = """\
+ ... The "or" operator
+ ... =================
+ ...
+ ... Here is an (erroneous) example of the "or" operator in action:
+ ...
+ ... ===== ===== ======
+ ... \ A or B
+ ... --------------------
+ ... A B Result
+ ... ===== ===== ======
+ ... False False True
+ ... True False True
+ ... False True False
+ ... True True True
+ ... ===== ===== ======
+ ... """
+
+ >>> document = manuel.Document(source_with_errors, location='fake.txt')
+ >>> find_tables(document)
+ >>> region = list(document)[1]
+
+This time the evaluator records errors:
+
+ >>> evaluate_table(region, document, {})
+ >>> region.evaluated
+ [<TableError object at ...>]
+
+
+Formatting Errors
+=================
+
+Now that we can parse the tables and evaluate them, we need to be able to
+display the results in a readable fashion.
+
+.. code-block:: python
+
+ def format_table_errors(document):
+ for region in document:
+ if not isinstance(region.evaluated, TableErrors):
+ continue
+
+ # if there were no errors, skip this table
+ if not region.evaluated:
+ continue
+
+ messages = []
+ for error in region.evaluated:
+ messages.append('%s, line %d: expected %r, got %r instead.' % (
+ error.location, error.lineno, error.expected, error.got))
+
+ sep = '\n '
+ header = 'when evaluating table at %s, line %d' % (
+ document.location, region.lineno)
+ region.formatted = header + sep + sep.join(messages)
+
+
+Now we can see how the formatted results look.
+
+ >>> format_table_errors(document)
+ >>> print region.formatted,
+ when evaluating table at fake.txt, line 6
+ fake.txt, line 11: expected True, got False instead.
+ fake.txt, line 13: expected False, got True instead.
+
+
+All Together Now
+================
+
+All the pieces (parsing, evaluating, and formatting) are available now, so we
+just have to put them together into a single "Manuel" object.
+
+ >>> m = manuel.Manuel(parsers=[find_tables], evaluaters=[evaluate_table],
+ ... formatters=[format_table_errors])
+
+Now we can create a fresh document and tell it to do all the above steps with
+our Manuel instance.
+
+ >>> document = manuel.Document(source_with_errors, location='fake.txt')
+ >>> document.process_with(m, globs={})
+ >>> print document.formatted(),
+ when evaluating table at fake.txt, line 6
+ fake.txt, line 11: expected True, got False instead.
+ fake.txt, line 13: expected False, got True instead.
+
+Of course, if there were no errors, nothing would be reported:
+
+ >>> document = manuel.Document(source, location='fake.txt')
+ >>> document.process_with(m, globs={})
+ >>> print document.formatted()
+
+If we wanted to use our new table tests in a file named "table-example.txt" and
+include them in a unittest TestSuite, it would look something like this:
+
+.. code-block:: python
+
+ import unittest
+ import manuel.testing
+
+ suite = unittest.TestSuite()
+ suite = manuel.testing.TestSuite(m, 'table-example.txt')
+
+If the tests are run (e.g., by a test runner), everything works.
+
+ >>> suite.run(unittest.TestResult())
+ <unittest.TestResult run=1 errors=0 failures=0>
Property changes on: manuel/trunk/src/manuel/table-example.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Modified: manuel/trunk/src/manuel/testing.py
===================================================================
--- manuel/trunk/src/manuel/testing.py 2009-06-06 13:39:45 UTC (rev 100663)
+++ manuel/trunk/src/manuel/testing.py 2009-06-06 17:00:25 UTC (rev 100664)
@@ -81,8 +81,15 @@
suite = unittest.TestSuite()
- # inspect the stack frame to find the module that called this function
- calling_module = doctest._normalize_module(None, depth=2)
+ # walk up the stack frame to find the module that called this function
+ depth = 2
+ for depth in range(2, 5):
+ try:
+ calling_module = doctest._normalize_module(None, depth=depth)
+ except KeyError:
+ continue
+ else:
+ break
for path in paths:
abs_path = doctest._module_relative_path(calling_module, path)
Modified: manuel/trunk/src/manuel/tests.py
===================================================================
--- manuel/trunk/src/manuel/tests.py 2009-06-06 13:39:45 UTC (rev 100663)
+++ manuel/trunk/src/manuel/tests.py 2009-06-06 17:00:25 UTC (rev 100664)
@@ -1,5 +1,6 @@
from zope.testing import renormalizing
import manuel
+import manuel.codeblock
import manuel.doctest
import manuel.testing
import re
@@ -25,4 +26,8 @@
m = manuel.doctest.Manuel(optionflags=optionflags, checker=checker)
suite.addTest(manuel.testing.TestSuite(m, *tests))
+ # Run the table example with doctest plus the codeblock extension.
+ m = manuel.doctest.Manuel(optionflags=optionflags, checker=checker)
+ m.extend(manuel.codeblock.Manuel())
+ suite.addTest(manuel.testing.TestSuite(m, 'table-example.txt'))
return suite
More information about the Checkins
mailing list