1a380bf10Simarom"""Use the Doctest plugin with ``--with-doctest`` or the NOSE_WITH_DOCTEST
2a380bf10Simaromenvironment variable to enable collection and execution of :mod:`doctests
3a380bf10Simarom<doctest>`.  Because doctests are usually included in the tested package
4a380bf10Simarom(instead of being grouped into packages or modules of their own), nose only
5a380bf10Simaromlooks for them in the non-test packages it discovers in the working directory.
7a380bf10SimaromDoctests may also be placed into files other than python modules, in which
8a380bf10Simaromcase they can be collected and executed by using the ``--doctest-extension``
9a380bf10Simaromswitch or NOSE_DOCTEST_EXTENSION environment variable to indicate which file
10a380bf10Simaromextension(s) to load.
12a380bf10SimaromWhen loading doctests from non-module files, use the ``--doctest-fixtures``
13a380bf10Simaromswitch to specify how to find modules containing fixtures for the tests. A
14a380bf10Simarommodule name will be produced by appending the value of that switch to the base
15a380bf10Simaromname of each doctest file loaded. For example, a doctest file "widgets.rst"
16a380bf10Simaromwith the switch ``--doctest_fixtures=_fixt`` will load fixtures from the module
19a380bf10SimaromA fixtures module may define any or all of the following functions:
21a380bf10Simarom* setup([module]) or setup_module([module])
23a380bf10Simarom  Called before the test runs. You may raise SkipTest to skip all tests.
25a380bf10Simarom* teardown([module]) or teardown_module([module])
27a380bf10Simarom  Called after the test runs, if setup/setup_module did not raise an
28a380bf10Simarom  unhandled exception.
30a380bf10Simarom* setup_test(test)
32a380bf10Simarom  Called before the test. NOTE: the argument passed is a
33a380bf10Simarom  doctest.DocTest instance, *not* a unittest.TestCase.
35a380bf10Simarom* teardown_test(test)
37a380bf10Simarom  Called after the test, if setup_test did not raise an exception. NOTE: the
38a380bf10Simarom  argument passed is a doctest.DocTest instance, *not* a unittest.TestCase.
40a380bf10SimaromDoctests are run like any other test, with the exception that output
41a380bf10Simaromcapture does not work; doctest does its own output capture while running a
44a380bf10Simarom.. note ::
46a380bf10Simarom   See :doc:`../doc_tests/test_doctest_fixtures/doctest_fixtures` for
47a380bf10Simarom   additional documentation and examples.
52a380bf10Simaromimport logging
53a380bf10Simaromimport os
54a380bf10Simaromimport sys
55a380bf10Simaromimport unittest
56a380bf10Simaromfrom inspect import getmodule
57a380bf10Simaromfrom nose.plugins.base import Plugin
58a380bf10Simaromfrom nose.suite import ContextList
59a380bf10Simaromfrom nose.util import anyp, getpackage, test_address, resolve_name, \
60a380bf10Simarom     src, tolist, isproperty
62a380bf10Simarom    from io import StringIO
63a380bf10Simaromexcept ImportError:
64a380bf10Simarom    from io import StringIO
65a380bf10Simaromimport sys
66a380bf10Simaromimport builtins as builtin_mod
68a380bf10Simaromlog = logging.getLogger(__name__)
71a380bf10Simarom    import doctest
72a380bf10Simarom    doctest.DocTestCase
73a380bf10Simarom    # system version of doctest is acceptable, but needs a monkeypatch
74a380bf10Simaromexcept (ImportError, AttributeError):
75a380bf10Simarom    # system version is too old
76a380bf10Simarom    import nose.ext.dtcompat as doctest
80a380bf10Simarom# Doctest and coverage don't get along, so we need to create
81a380bf10Simarom# a monkeypatch that will replace the part of doctest that
82a380bf10Simarom# interferes with coverage reports.
84a380bf10Simarom# The monkeypatch is based on this zope patch:
85a380bf10Simarom# http://svn.zope.org/Zope3/trunk/src/zope/testing/doctest.py?rev=28679&r1=28703&r2=28705
87a380bf10Simarom_orp = doctest._OutputRedirectingPdb
89a380bf10Simaromclass NoseOutputRedirectingPdb(_orp):
90a380bf10Simarom    def __init__(self, out):
91a380bf10Simarom        self.__debugger_used = False
92a380bf10Simarom        _orp.__init__(self, out)
94a380bf10Simarom    def set_trace(self):
95a380bf10Simarom        self.__debugger_used = True
96a380bf10Simarom        _orp.set_trace(self, sys._getframe().f_back)
98a380bf10Simarom    def set_continue(self):
99a380bf10Simarom        # Calling set_continue unconditionally would break unit test
100a380bf10Simarom        # coverage reporting, as Bdb.set_continue calls sys.settrace(None).
101a380bf10Simarom        if self.__debugger_used:
102a380bf10Simarom            _orp.set_continue(self)
103a380bf10Simaromdoctest._OutputRedirectingPdb = NoseOutputRedirectingPdb
106a380bf10Simaromclass DoctestSuite(unittest.TestSuite):
107a380bf10Simarom    """
108a380bf10Simarom    Doctest suites are parallelizable at the module or file level only,
109a380bf10Simarom    since they may be attached to objects that are not individually
110a380bf10Simarom    addressable (like properties). This suite subclass is used when
111a380bf10Simarom    loading doctests from a module to ensure that behavior.
113a380bf10Simarom    This class is used only if the plugin is not fully prepared;
114a380bf10Simarom    in normal use, the loader's suiteClass is used.
116a380bf10Simarom    """
117a380bf10Simarom    can_split = False
119a380bf10Simarom    def __init__(self, tests=(), context=None, can_split=False):
120a380bf10Simarom        self.context = context
121a380bf10Simarom        self.can_split = can_split
122a380bf10Simarom        unittest.TestSuite.__init__(self, tests=tests)
124a380bf10Simarom    def address(self):
125a380bf10Simarom        return test_address(self.context)
127a380bf10Simarom    def __iter__(self):
128a380bf10Simarom        # 2.3 compat
129a380bf10Simarom        return iter(self._tests)
131a380bf10Simarom    def __str__(self):
132a380bf10Simarom        return str(self._tests)
135a380bf10Simaromclass Doctest(Plugin):
136a380bf10Simarom    """
137a380bf10Simarom    Activate doctest plugin to find and run doctests in non-test modules.
138a380bf10Simarom    """
139a380bf10Simarom    extension = None
140a380bf10Simarom    suiteClass = DoctestSuite
142a380bf10Simarom    def options(self, parser, env):
143a380bf10Simarom        """Register commmandline options.
144a380bf10Simarom        """
145a380bf10Simarom        Plugin.options(self, parser, env)
146a380bf10Simarom        parser.add_option('--doctest-tests', action='store_true',
147a380bf10Simarom                          dest='doctest_tests',
148a380bf10Simarom                          default=env.get('NOSE_DOCTEST_TESTS'),
149a380bf10Simarom                          help="Also look for doctests in test modules. "
150a380bf10Simarom                          "Note that classes, methods and functions should "
151a380bf10Simarom                          "have either doctests or non-doctest tests, "
152a380bf10Simarom                          "not both. [NOSE_DOCTEST_TESTS]")
153a380bf10Simarom        parser.add_option('--doctest-extension', action="append",
154a380bf10Simarom                          dest="doctestExtension",
155a380bf10Simarom                          metavar="EXT",
156a380bf10Simarom                          help="Also look for doctests in files with "
157a380bf10Simarom                          "this extension [NOSE_DOCTEST_EXTENSION]")
158a380bf10Simarom        parser.add_option('--doctest-result-variable',
159a380bf10Simarom                          dest='doctest_result_var',
160a380bf10Simarom                          default=env.get('NOSE_DOCTEST_RESULT_VAR'),
161a380bf10Simarom                          metavar="VAR",
162a380bf10Simarom                          help="Change the variable name set to the result of "
163a380bf10Simarom                          "the last interpreter command from the default '_'. "
164a380bf10Simarom                          "Can be used to avoid conflicts with the _() "
165a380bf10Simarom                          "function used for text translation. "
166a380bf10Simarom                          "[NOSE_DOCTEST_RESULT_VAR]")
167a380bf10Simarom        parser.add_option('--doctest-fixtures', action="store",
168a380bf10Simarom                          dest="doctestFixtures",
169a380bf10Simarom                          metavar="SUFFIX",
170a380bf10Simarom                          help="Find fixtures for a doctest file in module "
171a380bf10Simarom                          "with this name appended to the base name "
172a380bf10Simarom                          "of the doctest file")
173a380bf10Simarom        parser.add_option('--doctest-options', action="append",
174a380bf10Simarom                          dest="doctestOptions",
175a380bf10Simarom                          metavar="OPTIONS",
176a380bf10Simarom                          help="Specify options to pass to doctest. " +
177a380bf10Simarom                          "Eg. '+ELLIPSIS,+NORMALIZE_WHITESPACE'")
178a380bf10Simarom        # Set the default as a list, if given in env; otherwise
179a380bf10Simarom        # an additional value set on the command line will cause
180a380bf10Simarom        # an error.
181a380bf10Simarom        env_setting = env.get('NOSE_DOCTEST_EXTENSION')
182a380bf10Simarom        if env_setting is not None:
183a380bf10Simarom            parser.set_defaults(doctestExtension=tolist(env_setting))
185a380bf10Simarom    def configure(self, options, config):
186a380bf10Simarom        """Configure plugin.
187a380bf10Simarom        """
188a380bf10Simarom        Plugin.configure(self, options, config)
189a380bf10Simarom        self.doctest_result_var = options.doctest_result_var
190a380bf10Simarom        self.doctest_tests = options.doctest_tests
191a380bf10Simarom        self.extension = tolist(options.doctestExtension)
192a380bf10Simarom        self.fixtures = options.doctestFixtures
193a380bf10Simarom        self.finder = doctest.DocTestFinder()
194a380bf10Simarom        self.optionflags = 0
195a380bf10Simarom        if options.doctestOptions:
196a380bf10Simarom            flags = ",".join(options.doctestOptions).split(',')
197a380bf10Simarom            for flag in flags:
198a380bf10Simarom                if not flag or flag[0] not in '+-':
199a380bf10Simarom                    raise ValueError(
200a380bf10Simarom                        "Must specify doctest options with starting " +
201a380bf10Simarom                        "'+' or '-'.  Got %s" % (flag,))
202a380bf10Simarom                mode, option_name = flag[0], flag[1:]
203a380bf10Simarom                option_flag = doctest.OPTIONFLAGS_BY_NAME.get(option_name)
204a380bf10Simarom                if not option_flag:
205a380bf10Simarom                    raise ValueError("Unknown doctest option %s" %
206a380bf10Simarom                                     (option_name,))
207a380bf10Simarom                if mode == '+':
208a380bf10Simarom                    self.optionflags |= option_flag
209a380bf10Simarom                elif mode == '-':
210a380bf10Simarom                    self.optionflags &= ~option_flag
212a380bf10Simarom    def prepareTestLoader(self, loader):
213a380bf10Simarom        """Capture loader's suiteClass.
215a380bf10Simarom        This is used to create test suites from doctest files.
217a380bf10Simarom        """
218a380bf10Simarom        self.suiteClass = loader.suiteClass
220a380bf10Simarom    def loadTestsFromModule(self, module):
221a380bf10Simarom        """Load doctests from the module.
222a380bf10Simarom        """
223a380bf10Simarom        log.debug("loading from %s", module)
224a380bf10Simarom        if not self.matches(module.__name__):
225a380bf10Simarom            log.debug("Doctest doesn't want module %s", module)
226a380bf10Simarom            return
227a380bf10Simarom        try:
228a380bf10Simarom            tests = self.finder.find(module)
229a380bf10Simarom        except AttributeError:
230a380bf10Simarom            log.exception("Attribute error loading from %s", module)
231a380bf10Simarom            # nose allows module.__test__ = False; doctest does not and throws
232a380bf10Simarom            # AttributeError
233a380bf10Simarom            return
234a380bf10Simarom        if not tests:
235a380bf10Simarom            log.debug("No tests found in %s", module)
236a380bf10Simarom            return
237a380bf10Simarom        tests.sort()
238a380bf10Simarom        module_file = src(module.__file__)
239a380bf10Simarom        # FIXME this breaks the id plugin somehow (tests probably don't
240a380bf10Simarom        # get wrapped in result proxy or something)
241a380bf10Simarom        cases = []
242a380bf10Simarom        for test in tests:
243a380bf10Simarom            if not test.examples:
244a380bf10Simarom                continue
245a380bf10Simarom            if not test.filename:
246a380bf10Simarom                test.filename = module_file
247a380bf10Simarom            cases.append(DocTestCase(test,
248a380bf10Simarom                                     optionflags=self.optionflags,
249a380bf10Simarom                                     result_var=self.doctest_result_var))
250a380bf10Simarom        if cases:
251a380bf10Simarom            yield self.suiteClass(cases, context=module, can_split=False)
253a380bf10Simarom    def loadTestsFromFile(self, filename):
254a380bf10Simarom        """Load doctests from the file.
256a380bf10Simarom        Tests are loaded only if filename's extension matches
257a380bf10Simarom        configured doctest extension.
259a380bf10Simarom        """
260a380bf10Simarom        if self.extension and anyp(filename.endswith, self.extension):
261a380bf10Simarom            name = os.path.basename(filename)
262a380bf10Simarom            dh = open(filename)
263a380bf10Simarom            try:
264a380bf10Simarom                doc = dh.read()
265a380bf10Simarom            finally:
266a380bf10Simarom                dh.close()
268a380bf10Simarom            fixture_context = None
269a380bf10Simarom            globs = {'__file__': filename}
270a380bf10Simarom            if self.fixtures:
271a380bf10Simarom                base, ext = os.path.splitext(name)
272a380bf10Simarom                dirname = os.path.dirname(filename)
273a380bf10Simarom                sys.path.append(dirname)
274a380bf10Simarom                fixt_mod = base + self.fixtures
275a380bf10Simarom                try:
276a380bf10Simarom                    fixture_context = __import__(
277a380bf10Simarom                        fixt_mod, globals(), locals(), ["nop"])
278a380bf10Simarom                except ImportError as e:
279a380bf10Simarom                    log.debug(
280a380bf10Simarom                        "Could not import %s: %s (%s)", fixt_mod, e, sys.path)
281a380bf10Simarom                log.debug("Fixture module %s resolved to %s",
282a380bf10Simarom                          fixt_mod, fixture_context)
283a380bf10Simarom                if hasattr(fixture_context, 'globs'):
284a380bf10Simarom                    globs = fixture_context.globs(globs)
285a380bf10Simarom            parser = doctest.DocTestParser()
286a380bf10Simarom            test = parser.get_doctest(
287a380bf10Simarom                doc, globs=globs, name=name,
288a380bf10Simarom                filename=filename, lineno=0)
289a380bf10Simarom            if test.examples:
290a380bf10Simarom                case = DocFileCase(
291a380bf10Simarom                    test,
292a380bf10Simarom                    optionflags=self.optionflags,
293a380bf10Simarom                    setUp=getattr(fixture_context, 'setup_test', None),
294a380bf10Simarom                    tearDown=getattr(fixture_context, 'teardown_test', None),
295a380bf10Simarom                    result_var=self.doctest_result_var)
296a380bf10Simarom                if fixture_context:
297a380bf10Simarom                    yield ContextList((case,), context=fixture_context)
298a380bf10Simarom                else:
299a380bf10Simarom                    yield case
300a380bf10Simarom            else:
301a380bf10Simarom                yield False # no tests to load
303a380bf10Simarom    def makeTest(self, obj, parent):
304a380bf10Simarom        """Look for doctests in the given object, which will be a
305a380bf10Simarom        function, method or class.
306a380bf10Simarom        """
307a380bf10Simarom        name = getattr(obj, '__name__', 'Unnammed %s' % type(obj))
308a380bf10Simarom        doctests = self.finder.find(obj, module=getmodule(parent), name=name)
309a380bf10Simarom        if doctests:
310a380bf10Simarom            for test in doctests:
311a380bf10Simarom                if len(test.examples) == 0:
312a380bf10Simarom                    continue
313a380bf10Simarom                yield DocTestCase(test, obj=obj, optionflags=self.optionflags,
314a380bf10Simarom                                  result_var=self.doctest_result_var)
316a380bf10Simarom    def matches(self, name):
317a380bf10Simarom        # FIXME this seems wrong -- nothing is ever going to
318a380bf10Simarom        # fail this test, since we're given a module NAME not FILE
319a380bf10Simarom        if name == '__init__.py':
320a380bf10Simarom            return False
321a380bf10Simarom        # FIXME don't think we need include/exclude checks here?
322a380bf10Simarom        return ((self.doctest_tests or not self.conf.testMatch.search(name)
323a380bf10Simarom                 or (self.conf.include
324a380bf10Simarom                     and [_f for _f in [inc.search(name)
325a380bf10Simarom                                 for inc in self.conf.include] if _f]))
326a380bf10Simarom                and (not self.conf.exclude
327a380bf10Simarom                     or not [_f for _f in [exc.search(name)
328a380bf10Simarom                                    for exc in self.conf.exclude] if _f]))
330a380bf10Simarom    def wantFile(self, file):
331a380bf10Simarom        """Override to select all modules and any file ending with
332a380bf10Simarom        configured doctest extension.
333a380bf10Simarom        """
334a380bf10Simarom        # always want .py files
335a380bf10Simarom        if file.endswith('.py'):
336a380bf10Simarom            return True
337a380bf10Simarom        # also want files that match my extension
338a380bf10Simarom        if (self.extension
339a380bf10Simarom            and anyp(file.endswith, self.extension)
340a380bf10Simarom            and (not self.conf.exclude
341a380bf10Simarom                 or not [_f for _f in [exc.search(file)
342a380bf10Simarom                                for exc in self.conf.exclude] if _f])):
343a380bf10Simarom            return True
344a380bf10Simarom        return None
347a380bf10Simaromclass DocTestCase(doctest.DocTestCase):
348a380bf10Simarom    """Overrides DocTestCase to
349a380bf10Simarom    provide an address() method that returns the correct address for
350a380bf10Simarom    the doctest case. To provide hints for address(), an obj may also
351a380bf10Simarom    be passed -- this will be used as the test object for purposes of
352a380bf10Simarom    determining the test address, if it is provided.
353a380bf10Simarom    """
354a380bf10Simarom    def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
355a380bf10Simarom                 checker=None, obj=None, result_var='_'):
356a380bf10Simarom        self._result_var = result_var
357a380bf10Simarom        self._nose_obj = obj
358a380bf10Simarom        super(DocTestCase, self).__init__(
359a380bf10Simarom            test, optionflags=optionflags, setUp=setUp, tearDown=tearDown,
360a380bf10Simarom            checker=checker)
362a380bf10Simarom    def address(self):
363a380bf10Simarom        if self._nose_obj is not None:
364a380bf10Simarom            return test_address(self._nose_obj)
365a380bf10Simarom        obj = resolve_name(self._dt_test.name)
367a380bf10Simarom        if isproperty(obj):
368a380bf10Simarom            # properties have no connection to the class they are in
369a380bf10Simarom            # so we can't just look 'em up, we have to first look up
370a380bf10Simarom            # the class, then stick the prop on the end
371a380bf10Simarom            parts = self._dt_test.name.split('.')
372a380bf10Simarom            class_name = '.'.join(parts[:-1])
373a380bf10Simarom            cls = resolve_name(class_name)
374a380bf10Simarom            base_addr = test_address(cls)
375a380bf10Simarom            return (base_addr[0], base_addr[1],
376a380bf10Simarom                    '.'.join([base_addr[2], parts[-1]]))
377a380bf10Simarom        else:
378a380bf10Simarom            return test_address(obj)
380a380bf10Simarom    # doctests loaded via find(obj) omit the module name
381a380bf10Simarom    # so we need to override id, __repr__ and shortDescription
382a380bf10Simarom    # bonus: this will squash a 2.3 vs 2.4 incompatiblity
383a380bf10Simarom    def id(self):
384a380bf10Simarom        name = self._dt_test.name
385a380bf10Simarom        filename = self._dt_test.filename
386a380bf10Simarom        if filename is not None:
387a380bf10Simarom            pk = getpackage(filename)
388a380bf10Simarom            if pk is None:
389a380bf10Simarom                return name
390a380bf10Simarom            if not name.startswith(pk):
391a380bf10Simarom                name = "%s.%s" % (pk, name)
392a380bf10Simarom        return name
394a380bf10Simarom    def __repr__(self):
395a380bf10Simarom        name = self.id()
396a380bf10Simarom        name = name.split('.')
397a380bf10Simarom        return "%s (%s)" % (name[-1], '.'.join(name[:-1]))
398a380bf10Simarom    __str__ = __repr__
400a380bf10Simarom    def shortDescription(self):
401a380bf10Simarom        return 'Doctest: %s' % self.id()
403a380bf10Simarom    def setUp(self):
404a380bf10Simarom        if self._result_var is not None:
405a380bf10Simarom            self._old_displayhook = sys.displayhook
406a380bf10Simarom            sys.displayhook = self._displayhook
407a380bf10Simarom        super(DocTestCase, self).setUp()
409a380bf10Simarom    def _displayhook(self, value):
410a380bf10Simarom        if value is None:
411a380bf10Simarom            return
412a380bf10Simarom        setattr(builtin_mod, self._result_var,  value)
413a380bf10Simarom        print(repr(value))
415a380bf10Simarom    def tearDown(self):
416a380bf10Simarom        super(DocTestCase, self).tearDown()
417a380bf10Simarom        if self._result_var is not None:
418a380bf10Simarom            sys.displayhook = self._old_displayhook
419a380bf10Simarom            delattr(builtin_mod, self._result_var)
422a380bf10Simaromclass DocFileCase(doctest.DocFileCase):
423a380bf10Simarom    """Overrides to provide address() method that returns the correct
424a380bf10Simarom    address for the doc file case.
425a380bf10Simarom    """
426a380bf10Simarom    def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
427a380bf10Simarom                 checker=None, result_var='_'):
428a380bf10Simarom        self._result_var = result_var
429a380bf10Simarom        super(DocFileCase, self).__init__(
430a380bf10Simarom            test, optionflags=optionflags, setUp=setUp, tearDown=tearDown,
431a380bf10Simarom            checker=None)
433a380bf10Simarom    def address(self):
434a380bf10Simarom        return (self._dt_test.filename, None, None)
436a380bf10Simarom    def setUp(self):
437a380bf10Simarom        if self._result_var is not None:
438a380bf10Simarom            self._old_displayhook = sys.displayhook
439a380bf10Simarom            sys.displayhook = self._displayhook
440a380bf10Simarom        super(DocFileCase, self).setUp()
442a380bf10Simarom    def _displayhook(self, value):
443a380bf10Simarom        if value is None:
444a380bf10Simarom            return
445a380bf10Simarom        setattr(builtin_mod, self._result_var, value)
446a380bf10Simarom        print(repr(value))
448a380bf10Simarom    def tearDown(self):
449a380bf10Simarom        super(DocFileCase, self).tearDown()
450a380bf10Simarom        if self._result_var is not None:
451a380bf10Simarom            sys.displayhook = self._old_displayhook
452a380bf10Simarom            delattr(builtin_mod, self._result_var)