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"""
50
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 io import StringIO
63except ImportError:
64    from io import StringIO
65import sys
66import builtins 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 as 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 [_f for _f in [inc.search(name)
325                                 for inc in self.conf.include] if _f]))
326                and (not self.conf.exclude
327                     or not [_f for _f in [exc.search(name)
328                                    for exc in self.conf.exclude] if _f]))
329
330    def wantFile(self, file):
331        """Override to select all modules and any file ending with
332        configured doctest extension.
333        """
334        # always want .py files
335        if file.endswith('.py'):
336            return True
337        # also want files that match my extension
338        if (self.extension
339            and anyp(file.endswith, self.extension)
340            and (not self.conf.exclude
341                 or not [_f for _f in [exc.search(file)
342                                for exc in self.conf.exclude] if _f])):
343            return True
344        return None
345
346
347class DocTestCase(doctest.DocTestCase):
348    """Overrides DocTestCase to
349    provide an address() method that returns the correct address for
350    the doctest case. To provide hints for address(), an obj may also
351    be passed -- this will be used as the test object for purposes of
352    determining the test address, if it is provided.
353    """
354    def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
355                 checker=None, obj=None, result_var='_'):
356        self._result_var = result_var
357        self._nose_obj = obj
358        super(DocTestCase, self).__init__(
359            test, optionflags=optionflags, setUp=setUp, tearDown=tearDown,
360            checker=checker)
361
362    def address(self):
363        if self._nose_obj is not None:
364            return test_address(self._nose_obj)
365        obj = resolve_name(self._dt_test.name)
366
367        if isproperty(obj):
368            # properties have no connection to the class they are in
369            # so we can't just look 'em up, we have to first look up
370            # the class, then stick the prop on the end
371            parts = self._dt_test.name.split('.')
372            class_name = '.'.join(parts[:-1])
373            cls = resolve_name(class_name)
374            base_addr = test_address(cls)
375            return (base_addr[0], base_addr[1],
376                    '.'.join([base_addr[2], parts[-1]]))
377        else:
378            return test_address(obj)
379
380    # doctests loaded via find(obj) omit the module name
381    # so we need to override id, __repr__ and shortDescription
382    # bonus: this will squash a 2.3 vs 2.4 incompatiblity
383    def id(self):
384        name = self._dt_test.name
385        filename = self._dt_test.filename
386        if filename is not None:
387            pk = getpackage(filename)
388            if pk is None:
389                return name
390            if not name.startswith(pk):
391                name = "%s.%s" % (pk, name)
392        return name
393
394    def __repr__(self):
395        name = self.id()
396        name = name.split('.')
397        return "%s (%s)" % (name[-1], '.'.join(name[:-1]))
398    __str__ = __repr__
399
400    def shortDescription(self):
401        return 'Doctest: %s' % self.id()
402
403    def setUp(self):
404        if self._result_var is not None:
405            self._old_displayhook = sys.displayhook
406            sys.displayhook = self._displayhook
407        super(DocTestCase, self).setUp()
408
409    def _displayhook(self, value):
410        if value is None:
411            return
412        setattr(builtin_mod, self._result_var,  value)
413        print(repr(value))
414
415    def tearDown(self):
416        super(DocTestCase, self).tearDown()
417        if self._result_var is not None:
418            sys.displayhook = self._old_displayhook
419            delattr(builtin_mod, self._result_var)
420
421
422class DocFileCase(doctest.DocFileCase):
423    """Overrides to provide address() method that returns the correct
424    address for the doc file case.
425    """
426    def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
427                 checker=None, result_var='_'):
428        self._result_var = result_var
429        super(DocFileCase, self).__init__(
430            test, optionflags=optionflags, setUp=setUp, tearDown=tearDown,
431            checker=None)
432
433    def address(self):
434        return (self._dt_test.filename, None, None)
435
436    def setUp(self):
437        if self._result_var is not None:
438            self._old_displayhook = sys.displayhook
439            sys.displayhook = self._displayhook
440        super(DocFileCase, self).setUp()
441
442    def _displayhook(self, value):
443        if value is None:
444            return
445        setattr(builtin_mod, self._result_var, value)
446        print(repr(value))
447
448    def tearDown(self):
449        super(DocFileCase, self).tearDown()
450        if self._result_var is not None:
451            sys.displayhook = self._old_displayhook
452            delattr(builtin_mod, self._result_var)
453