[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