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 [] +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