[Checkins] SVN: zope.testing/branches/benji-add-footnote-interpretation-to-doctest/src/zope/testing/doctest.py - fix remaining bugs (that I'm aware of)

Benji York benji at zope.com
Mon Jul 10 13:42:11 EDT 2006


Log message for revision 69075:
   - fix remaining bugs (that I'm aware of)
   - add more extensive tests
   - don't evaluate code in footnotes at the point of definition
   - warn if a footnote (with code) is defined and never used
  

Changed:
  U   zope.testing/branches/benji-add-footnote-interpretation-to-doctest/src/zope/testing/doctest.py

-=-
Modified: zope.testing/branches/benji-add-footnote-interpretation-to-doctest/src/zope/testing/doctest.py
===================================================================
--- zope.testing/branches/benji-add-footnote-interpretation-to-doctest/src/zope/testing/doctest.py	2006-07-10 15:04:49 UTC (rev 69074)
+++ zope.testing/branches/benji-add-footnote-interpretation-to-doctest/src/zope/testing/doctest.py	2006-07-10 17:42:11 UTC (rev 69075)
@@ -105,6 +105,9 @@
 warnings.filterwarnings("ignore", "is_private", DeprecationWarning,
                         __name__, 0)
 
+class UnusedFootnoteWarning(Warning):
+    """Warn about a footnote that is defined, but never referenced."""
+
 real_pdb_set_trace = pdb.set_trace
 
 # There are 4 basic classes:
@@ -573,7 +576,7 @@
     _FOOTNOTE_DEFINITION_RE = re.compile(
         r'^\.\.\s*\[\s*([^\]]+)\s*\].*$', re.MULTILINE)
 
-    # End of footnote regex.
+    # End of footnote regex.   Just looks for any unindented line.
     _FOOTNOTE_END_RE = re.compile(r'^\S+', re.MULTILINE)
 
     def parse(self, string, name='<string>', optionflags=0):
@@ -615,53 +618,68 @@
         output.append(string[charno:])
 
         if optionflags & INTERPRET_FOOTNOTES:
-            new_output = []
-
             footnotes = {}
             in_footnote = False
-            for i, x in enumerate(output):
+            # collect all the footnotes
+            for x in output:
                 if in_footnote:
+                    footnote.append(x)
+                    # we're collecting prose and examples for a footnote
                     if isinstance(x, Example):
-                        footnote.append(x)
+                        x._footnote_name = name
                     elif self._FOOTNOTE_END_RE.search(x):
+                        # this looks like prose that ends a footnote
                         in_footnote = False
                         footnotes[name] = footnote
                         del name
                         del footnote
-                    else:
-                        footnote.append(x)
 
                 if not in_footnote:
-                    s = x
-                    if not isinstance(s, Example):
+                    if not isinstance(x, Example):
                         matches = list(
-                            self._FOOTNOTE_DEFINITION_RE.finditer(s))
+                            self._FOOTNOTE_DEFINITION_RE.finditer(x))
 
                         if matches:
-                            # all but the last one don't have any associated
-                            # code
+                            # all but the last one don't have any code
+                            # note: we intentionally reuse the "leaked" value
+                            # of match below
                             for match in matches:
                                 footnotes[match.group(1)] = []
 
-                            match = matches[-1]
-                            s = s[match.end()+1:]
+                            # XXX is this code (through "continue") needed?
 
-                            if self._FOOTNOTE_END_RE.search(s):
+                            # throw away all the prose leading up to the last
+                            # footnote definition in the prose, this is so we
+                            # don't confuse a previous footnote end with the
+                            # end of *this* footnote
+                            tail = x[match.end()+1:]
+
+                            if self._FOOTNOTE_END_RE.search(tail):
                                 # over before it began
+                                raise 'hmm'
                                 continue
 
                             in_footnote = True
                             name = match.group(1)
                             footnote = []
 
-                new_output.append(x)
+            # if we were still collecting a footnote when the loop ended,
+            # stash it away so it's not lost
+            if in_footnote:
+                footnotes[name] = footnote
 
-            output = new_output
+            # inject each footnote into the point(s) at which it is referenced
             new_output = []
+            defined_footnotes = []
+            used_footnotes = []
             for x in output:
-                new_output.append(x)
-
-                if not isinstance(x, Example):
+                if isinstance(x, Example):
+                    # we don't want to execute footnotes where they're defined
+                    if hasattr(x, '_footnote_name'):
+                        defined_footnotes.append(x._footnote_name)
+                        continue
+                else:
+                    m = None
                     for m in self._FOOTNOTE_REFERENCE_RE.finditer(x):
                         name = m.group(1)
                         if name not in footnotes:
@@ -669,11 +687,21 @@
                                 'A footnote was referred to, but never'
                                 ' defined: %r' % name)
 
+                        new_output.append(x)
                         new_output.extend(footnotes[name])
-                        new_output.append('') # keep the text/example balance
+                        used_footnotes.append(name)
+                    if m is not None:
+                        continue
 
+                new_output.append(x)
             output = new_output
 
+            # make sure that all of the footnotes found were actually used
+            unused_footnotes = set(defined_footnotes) - set(used_footnotes)
+            for x in unused_footnotes:
+                warnings.warn('a footnote was defined, but never used: %r' % x,
+                              UnusedFootnoteWarning)
+
         return output
 
     def get_doctest(self, string, globs, name, filename, lineno,
@@ -2819,7 +2847,10 @@
            }
 
 def _test_footnotes():
-    """
+    '''
+    Footnotes
+    =========
+
     If the INTERPRET_FOOTNOTES flag is passed as part of optionflags, then
     footnotes will be looked up and their code injected at each point of
     reference.  For example:
@@ -2831,13 +2862,13 @@
         >>> counter
         1
 
-    .. [1] and here we set up the value
+    .. [1] and here we increment ``counter``
         >>> counter += 1
 
     Footnotes can also be referenced after they are defined: [1]_
 
         >>> counter
-        3
+        2
 
     Footnotes can also be "citations", which just means that the value in
     the brackets is alphanumeric: [citation]_
@@ -2863,49 +2894,130 @@
 
         >>> one = 1
 
-        And now another (note indentation to make this part of the
-        footnote):
+        and now another (note indentation to make this part of the footnote):
 
         >>> two = 2
 
-        even more:
+        and a third:
 
         >>> three = 3
 
-    Footnotes can have code that starts with no prose between. [quick code]_
 
-    .. [quick code] foo
+    Parsing Details
+    ---------------
 
-        >>> print 'this is some code'
-        this is some code
+    If the INTERPRET_FOOTNOTES optionflag isn't set, footnotes are ignored.
 
-    Footnotes can be back-to-back [first]_ [second]_
-    .. [first]
-    .. [second]
-        >>> 1+1
-        2
+    >>> doctest = """
+    ... This is a doctest. [1]_
+    ...
+    ...     >>> print var
+    ...
+    ... .. [1] a footnote
+    ...     Here we set up the variable
+    ...
+    ...     >>> var = 1
+    ... """
 
-    .. [no code] Footnotes can also be defined with no code.
+    >>> print_structure(doctest)
+    Prose| This is a doctest. [1]_
+    Code | print var
+    Prose| .. [1] a footnote
+    Code | var = 1
+    Prose|
 
-    Footnotes can also refer to other footnotes [other]_
+    If INTERPRET_FOOTNOTES is set, footnotes are also copied to the point at
+    which they are referenced.
 
-    .. [other] This note refer to another one [1]_
-        >>> 'foo'
-        'foo'
+    >>> print_structure(doctest, optionflags=INTERPRET_FOOTNOTES)
+    Prose| This is a doctest. [1]_
+    Code | var = 1
+    Prose|
+    Code | print var
+    Prose| .. [1] a footnote
+    Prose|
 
-    Or they can even refer to themselves [recursive]_
+    >>> print_structure("""
+    ... Footnotes can have code that starts with no prose. [quick code]_
+    ...
+    ... .. [quick code]
+    ...     >>> print 'this is some code'
+    ...     this is some code
+    ... """, optionflags=INTERPRET_FOOTNOTES)
+    Prose| Footnotes can have code that starts with no prose. [quick code]_
+    Code | print 'this is some code'
+    Prose|
+    Prose|
 
-    .. [recursive] this one refers to itself [recursive]_
-        >>> 'bar'
-        'bar'
+    >>> print_structure("""
+    ... Footnotes can be back-to-back [first]_ [second]_
+    ... .. [first]
+    ... .. [second]
+    ...     >>> 1+1
+    ...     2
+    ... """, optionflags=INTERPRET_FOOTNOTES)
+    Prose| Footnotes can be back-to-back [first]_ [second]_
+    Prose| Footnotes can be back-to-back [first]_ [second]_
+    Code | 1+1
+    Prose|
+    Prose|
 
-    The last footnote works too
+    >>> print_structure("""
+    ... .. [no code] Footnotes can also be defined with no code.
+    ... """, optionflags=INTERPRET_FOOTNOTES)
+    Prose| .. [no code] Footnotes can also be defined with no code.
 
-    .. [last] This is the last one
-        >>> 'baz'
-        'baz'
-    """
+    If there are multiple footnotes with no code, then one with code, they are
+    parsed correctly.
 
+    >>> print_structure("""
+    ... I'd like some code to go here [some code]_
+    ... .. [no code 1] Footnotes can also be defined with no code.
+    ... .. [no code 2] Footnotes can also be defined with no code.
+    ... .. [no code 3] Footnotes can also be defined with no code.
+    ... .. [some code]
+    ...     >>> print 'hi'
+    ...     hi
+    ... """, optionflags=INTERPRET_FOOTNOTES)
+    Prose| I'd like some code to go here [some code]_
+    Code | print 'hi'
+    Prose|
+    Prose|
+
+    The "autonumber" flavor of labels works too.
+
+    >>> print_structure("""
+    ... Numbered footnotes are good [#foo]_
+    ... .. [#foo]
+    ...     >>> print 'hi'
+    ...     hi
+    ... """, optionflags=INTERPRET_FOOTNOTES)
+    Prose| Numbered footnotes are good [#foo]_
+    Code | print 'hi'
+    Prose|
+    Prose|
+    '''
+
+
+def print_structure(doctest, optionflags=0):
+    def preview(s):
+        first_line = s.strip().split('\n')[0]
+        MAX_LENGTH = 70
+        if len(first_line) <= MAX_LENGTH:
+            return first_line
+
+        return '%s...' % first_line[:MAX_LENGTH].strip()
+
+    parser = DocTestParser()
+    for x in parser.parse(doctest, optionflags=optionflags):
+        if isinstance(x, Example):
+            result = 'Code | ' + preview(x.source)
+        else:
+            result = 'Prose| ' + preview(x)
+
+        print result.strip()
+
+
 def _test():
     r = unittest.TextTestRunner()
     r.run(DocTestSuite(optionflags=INTERPRET_FOOTNOTES))



More information about the Checkins mailing list