[issue12055] doctest not working on nested functions

Baptiste Carvello report at bugs.python.org
Mon May 23 18:15:06 CEST 2011


Baptiste Carvello <devel at baptiste-carvello.net> added the comment:

here is the patch for 2.7.

Dave: I don't know if or when the patch will be accepted, as this is a new 
feature. In the meantime, you can easily patch your system. As the code changes 
are all in Python, you don't need to recompile. Going to your standard library 
directory and running a command like the following should do the trick:

filterdiff -i '*/doctest.py' issue12055-27.diff | patch

----------
Added file: http://bugs.python.org/file22085/issue12055-27.diff

_______________________________________
Python tracker <report at bugs.python.org>
<http://bugs.python.org/issue12055>
_______________________________________
-------------- next part --------------
diff --git a/Doc/library/doctest.rst b/Doc/library/doctest.rst
--- a/Doc/library/doctest.rst
+++ b/Doc/library/doctest.rst
@@ -280,7 +280,7 @@
 searched.  Objects imported into the module are not searched.
 
 In addition, if ``M.__test__`` exists and "is true", it must be a dict, and each
-entry maps a (string) name to a function object, class object, or string.
+entry maps a (string) name to a function object, code object, class object, or string.
 Function and class object docstrings found from ``M.__test__`` are searched, and
 strings are treated as if they were docstrings.  In output, a key ``K`` in
 ``M.__test__`` appears with name ::
@@ -293,6 +293,12 @@
 .. versionchanged:: 2.4
    A "private name" concept is deprecated and no longer documented.
 
+Any functions found are recursively searched similarly, to test docstrings in 
+their contained nested functions (nested functions exist as a code object constant).
+
+Any code objects found, be it in ``M.__test__`` or nested in a function, are recursively 
+searched similarly, to test docstrings in their contained nested functions.
+
 
 .. _doctest-finding-examples:
 
diff --git a/Lib/doctest.py b/Lib/doctest.py
--- a/Lib/doctest.py
+++ b/Lib/doctest.py
@@ -98,6 +98,7 @@
 import sys, traceback, inspect, linecache, os, re
 import unittest, difflib, pdb, tempfile
 import warnings
+import types
 from StringIO import StringIO
 from collections import namedtuple
 
@@ -814,7 +815,10 @@
         """
         # If name was not specified, then extract it from the object.
         if name is None:
-            name = getattr(obj, '__name__', None)
+            if type(obj) != types.CodeType:
+                name = getattr(obj, '__name__', None)
+            else:
+                name = getattr(obj, 'co_name', None)
             if name is None:
                 raise ValueError("DocTestFinder.find: name must be given "
                         "when obj.__name__ doesn't exist: %r" %
@@ -925,17 +929,34 @@
                     raise ValueError("DocTestFinder.find: __test__ keys "
                                      "must be strings: %r" %
                                      (type(valname),))
-                if not (inspect.isfunction(val) or inspect.isclass(val) or
-                        inspect.ismethod(val) or inspect.ismodule(val) or
-                        isinstance(val, basestring)):
+                if not (inspect.isfunction(val) or inspect.iscode(val) or
+                        inspect.isclass(val) or inspect.ismethod(val) or
+                        inspect.ismodule(val) or isinstance(val, basestring)):
                     raise ValueError("DocTestFinder.find: __test__ values "
-                                     "must be strings, functions, methods, "
+                                     "must be strings, functions, "
+                                     "code objects, methods, "
                                      "classes, or modules: %r" %
                                      (type(val),))
                 valname = '%s.__test__.%s' % (name, valname)
                 self._find(tests, val, valname, module, source_lines,
                            globs, seen)
 
+        # Look for tests in a function's contained objects.
+        if inspect.isfunction(obj) and self._recurse:
+            for val in obj.__code__.co_consts:
+                if inspect.iscode(val):
+                    valname = '%s.%s' % (name, val.co_name)
+                    self._find(tests, val, valname, module, source_lines,
+                               globs, seen)
+
+        # Look for tests in a code object's contained objects.
+        if inspect.iscode(obj) and self._recurse:
+            for val in obj.co_consts:
+                if inspect.iscode(val):
+                    valname = '%s.%s' % (name, val.co_name)
+                    self._find(tests, val, valname, module, source_lines,
+                               globs, seen)
+
         # Look for tests in a class's contained objects.
         if inspect.isclass(obj) and self._recurse:
             for valname, val in obj.__dict__.items():
@@ -962,6 +983,16 @@
         # then return None (no test for this object).
         if isinstance(obj, basestring):
             docstring = obj
+        elif inspect.iscode(obj):
+            try:
+                if obj.co_consts[0] is None:
+                    docstring = ''
+                else:
+                    docstring = obj.co_consts[0]
+                    if not isinstance(docstring, str):
+                        docstring = str(docstring)
+            except (TypeError, AttributeError, IndexError):
+                docstring = ''
         else:
             try:
                 if obj.__doc__ is None:
diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py
--- a/Lib/test/test_doctest.py
+++ b/Lib/test/test_doctest.py
@@ -25,6 +25,40 @@
     """
     return v+v
 
+def sample_outer_func(v):
+
+    def sample_nested_func(w):
+        """
+        Blah blah
+
+        >>> print sample_outer_func(22)(42)
+        64
+
+        Yee ha!
+        """
+        return v+w
+
+    return sample_nested_func
+
+def sample_outer_func_2(v):
+
+    def sample_nested_func(w):
+
+        def sample_doubly_nested_func(x):
+            """
+            Blah blah
+
+            >>> print sample_outer_func_2(22)(42)(28)
+            92
+
+            Yee ha!
+            """
+            return v+w+x
+
+        return sample_doubly_nested_func
+
+    return sample_nested_func
+
 class SampleClass:
     """
     >>> print 1
@@ -415,6 +449,51 @@
     >>> finder.find(no_examples) # doctest: +ELLIPSIS
     [<DocTest no_examples from ...:1 (no examples)>]
 
+If a function's code object contains code objects in co_consts, as is the 
+case for nested functions, docstrings are searched in those code objects
+
+    >>> tests = finder.find(sample_outer_func)
+    >>> for t in tests:
+    ...     print '%2s  %s' % (len(t.examples), t.name)
+     1  sample_outer_func.sample_nested_func
+    >>> e = tests[0].examples[0]
+    >>> (e.source, e.want, e.lineno)
+    ('print sample_outer_func(22)(42)\n', '64\n', 3)
+
+This also works for multiply nested functions
+
+    >>> tests = finder.find(sample_outer_func_2)
+    >>> for t in tests:
+    ...     print '%2s  %s' % (len(t.examples), t.name)
+     1  sample_outer_func_2.sample_nested_func.sample_doubly_nested_func
+    >>> e = tests[0].examples[0]
+    >>> (e.source, e.want, e.lineno)
+    ('print sample_outer_func_2(22)(42)(28)\n', '92\n', 3)
+
+Finding Tests in Code Objects
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Code objects are treated the same as functions
+
+    >>> finder = doctest.DocTestFinder()
+    >>> tests = finder.find(sample_func.__code__)
+    >>> for t in tests:
+    ...     print '%2s  %s' % (len(t.examples), t.name)
+     1  sample_func
+    >>> e = tests[0].examples[0]
+    >>> (e.source, e.want, e.lineno)
+    ('print sample_func(22)\n', '44\n', 3)
+
+Nested functions are supported
+
+    >>> tests = finder.find(sample_outer_func.__code__)
+    >>> for t in tests:
+    ...     print '%2s  %s' % (len(t.examples), t.name)
+     1  sample_outer_func.sample_nested_func
+    >>> e = tests[0].examples[0]
+    >>> (e.source, e.want, e.lineno)
+    ('print sample_outer_func(22)(42)\n', '64\n', 3)
+
 Finding Tests in Classes
 ~~~~~~~~~~~~~~~~~~~~~~~~
 For a class, DocTestFinder will create a test for the class's
@@ -470,7 +549,8 @@
     ...         ''',
     ...     '__test__': {
     ...         'd': '>>> print 6\n6\n>>> print 7\n7\n',
-    ...         'c': triple}})
+    ...         'c': triple,
+    ...         'sample_nested_func_codeobj': sample_outer_func.__code__.co_consts[1]}})
 
     >>> finder = doctest.DocTestFinder()
     >>> # Use module=test.test_doctest, to prevent doctest from
@@ -491,6 +571,7 @@
      1  some_module.SampleClass.get
      1  some_module.__test__.c
      2  some_module.__test__.d
+     1  some_module.__test__.sample_nested_func_codeobj
      1  some_module.sample_func
 
 Duplicate Removal


More information about the Python-bugs-list mailing list