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