1""" discover and run doctests in modules and test files."""
2from __future__ import absolute_import, division, print_function
3
4import traceback
5import sys
6import platform
7
8import pytest
9from _pytest._code.code import ExceptionInfo, ReprFileLocation, TerminalRepr
10from _pytest.fixtures import FixtureRequest
11
12
13DOCTEST_REPORT_CHOICE_NONE = "none"
14DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
15DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
16DOCTEST_REPORT_CHOICE_UDIFF = "udiff"
17DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure"
18
19DOCTEST_REPORT_CHOICES = (
20    DOCTEST_REPORT_CHOICE_NONE,
21    DOCTEST_REPORT_CHOICE_CDIFF,
22    DOCTEST_REPORT_CHOICE_NDIFF,
23    DOCTEST_REPORT_CHOICE_UDIFF,
24    DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
25)
26
27# Lazy definition of runner class
28RUNNER_CLASS = None
29
30
31def pytest_addoption(parser):
32    parser.addini(
33        "doctest_optionflags",
34        "option flags for doctests",
35        type="args",
36        default=["ELLIPSIS"],
37    )
38    parser.addini(
39        "doctest_encoding", "encoding used for doctest files", default="utf-8"
40    )
41    group = parser.getgroup("collect")
42    group.addoption(
43        "--doctest-modules",
44        action="store_true",
45        default=False,
46        help="run doctests in all .py modules",
47        dest="doctestmodules",
48    )
49    group.addoption(
50        "--doctest-report",
51        type=str.lower,
52        default="udiff",
53        help="choose another output format for diffs on doctest failure",
54        choices=DOCTEST_REPORT_CHOICES,
55        dest="doctestreport",
56    )
57    group.addoption(
58        "--doctest-glob",
59        action="append",
60        default=[],
61        metavar="pat",
62        help="doctests file matching pattern, default: test*.txt",
63        dest="doctestglob",
64    )
65    group.addoption(
66        "--doctest-ignore-import-errors",
67        action="store_true",
68        default=False,
69        help="ignore doctest ImportErrors",
70        dest="doctest_ignore_import_errors",
71    )
72    group.addoption(
73        "--doctest-continue-on-failure",
74        action="store_true",
75        default=False,
76        help="for a given doctest, continue to run after the first failure",
77        dest="doctest_continue_on_failure",
78    )
79
80
81def pytest_collect_file(path, parent):
82    config = parent.config
83    if path.ext == ".py":
84        if config.option.doctestmodules and not _is_setup_py(config, path, parent):
85            return DoctestModule(path, parent)
86    elif _is_doctest(config, path, parent):
87        return DoctestTextfile(path, parent)
88
89
90def _is_setup_py(config, path, parent):
91    if path.basename != "setup.py":
92        return False
93    contents = path.read()
94    return "setuptools" in contents or "distutils" in contents
95
96
97def _is_doctest(config, path, parent):
98    if path.ext in (".txt", ".rst") and parent.session.isinitpath(path):
99        return True
100    globs = config.getoption("doctestglob") or ["test*.txt"]
101    for glob in globs:
102        if path.check(fnmatch=glob):
103            return True
104    return False
105
106
107class ReprFailDoctest(TerminalRepr):
108
109    def __init__(self, reprlocation_lines):
110        # List of (reprlocation, lines) tuples
111        self.reprlocation_lines = reprlocation_lines
112
113    def toterminal(self, tw):
114        for reprlocation, lines in self.reprlocation_lines:
115            for line in lines:
116                tw.line(line)
117            reprlocation.toterminal(tw)
118
119
120class MultipleDoctestFailures(Exception):
121
122    def __init__(self, failures):
123        super(MultipleDoctestFailures, self).__init__()
124        self.failures = failures
125
126
127def _init_runner_class():
128    import doctest
129
130    class PytestDoctestRunner(doctest.DebugRunner):
131        """
132        Runner to collect failures.  Note that the out variable in this case is
133        a list instead of a stdout-like object
134        """
135
136        def __init__(
137            self, checker=None, verbose=None, optionflags=0, continue_on_failure=True
138        ):
139            doctest.DebugRunner.__init__(
140                self, checker=checker, verbose=verbose, optionflags=optionflags
141            )
142            self.continue_on_failure = continue_on_failure
143
144        def report_failure(self, out, test, example, got):
145            failure = doctest.DocTestFailure(test, example, got)
146            if self.continue_on_failure:
147                out.append(failure)
148            else:
149                raise failure
150
151        def report_unexpected_exception(self, out, test, example, exc_info):
152            failure = doctest.UnexpectedException(test, example, exc_info)
153            if self.continue_on_failure:
154                out.append(failure)
155            else:
156                raise failure
157
158    return PytestDoctestRunner
159
160
161def _get_runner(checker=None, verbose=None, optionflags=0, continue_on_failure=True):
162    # We need this in order to do a lazy import on doctest
163    global RUNNER_CLASS
164    if RUNNER_CLASS is None:
165        RUNNER_CLASS = _init_runner_class()
166    return RUNNER_CLASS(
167        checker=checker,
168        verbose=verbose,
169        optionflags=optionflags,
170        continue_on_failure=continue_on_failure,
171    )
172
173
174class DoctestItem(pytest.Item):
175
176    def __init__(self, name, parent, runner=None, dtest=None):
177        super(DoctestItem, self).__init__(name, parent)
178        self.runner = runner
179        self.dtest = dtest
180        self.obj = None
181        self.fixture_request = None
182
183    def setup(self):
184        if self.dtest is not None:
185            self.fixture_request = _setup_fixtures(self)
186            globs = dict(getfixture=self.fixture_request.getfixturevalue)
187            for name, value in self.fixture_request.getfixturevalue(
188                "doctest_namespace"
189            ).items():
190                globs[name] = value
191            self.dtest.globs.update(globs)
192
193    def runtest(self):
194        _check_all_skipped(self.dtest)
195        self._disable_output_capturing_for_darwin()
196        failures = []
197        self.runner.run(self.dtest, out=failures)
198        if failures:
199            raise MultipleDoctestFailures(failures)
200
201    def _disable_output_capturing_for_darwin(self):
202        """
203        Disable output capturing. Otherwise, stdout is lost to doctest (#985)
204        """
205        if platform.system() != "Darwin":
206            return
207        capman = self.config.pluginmanager.getplugin("capturemanager")
208        if capman:
209            out, err = capman.suspend_global_capture(in_=True)
210            sys.stdout.write(out)
211            sys.stderr.write(err)
212
213    def repr_failure(self, excinfo):
214        import doctest
215
216        failures = None
217        if excinfo.errisinstance((doctest.DocTestFailure, doctest.UnexpectedException)):
218            failures = [excinfo.value]
219        elif excinfo.errisinstance(MultipleDoctestFailures):
220            failures = excinfo.value.failures
221
222        if failures is not None:
223            reprlocation_lines = []
224            for failure in failures:
225                example = failure.example
226                test = failure.test
227                filename = test.filename
228                if test.lineno is None:
229                    lineno = None
230                else:
231                    lineno = test.lineno + example.lineno + 1
232                message = type(failure).__name__
233                reprlocation = ReprFileLocation(filename, lineno, message)
234                checker = _get_checker()
235                report_choice = _get_report_choice(
236                    self.config.getoption("doctestreport")
237                )
238                if lineno is not None:
239                    lines = failure.test.docstring.splitlines(False)
240                    # add line numbers to the left of the error message
241                    lines = [
242                        "%03d %s" % (i + test.lineno + 1, x)
243                        for (i, x) in enumerate(lines)
244                    ]
245                    # trim docstring error lines to 10
246                    lines = lines[max(example.lineno - 9, 0):example.lineno + 1]
247                else:
248                    lines = [
249                        "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
250                    ]
251                    indent = ">>>"
252                    for line in example.source.splitlines():
253                        lines.append("??? %s %s" % (indent, line))
254                        indent = "..."
255                if isinstance(failure, doctest.DocTestFailure):
256                    lines += checker.output_difference(
257                        example, failure.got, report_choice
258                    ).split(
259                        "\n"
260                    )
261                else:
262                    inner_excinfo = ExceptionInfo(failure.exc_info)
263                    lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
264                    lines += traceback.format_exception(*failure.exc_info)
265                reprlocation_lines.append((reprlocation, lines))
266            return ReprFailDoctest(reprlocation_lines)
267        else:
268            return super(DoctestItem, self).repr_failure(excinfo)
269
270    def reportinfo(self):
271        return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name
272
273
274def _get_flag_lookup():
275    import doctest
276
277    return dict(
278        DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
279        DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
280        NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
281        ELLIPSIS=doctest.ELLIPSIS,
282        IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
283        COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
284        ALLOW_UNICODE=_get_allow_unicode_flag(),
285        ALLOW_BYTES=_get_allow_bytes_flag(),
286    )
287
288
289def get_optionflags(parent):
290    optionflags_str = parent.config.getini("doctest_optionflags")
291    flag_lookup_table = _get_flag_lookup()
292    flag_acc = 0
293    for flag in optionflags_str:
294        flag_acc |= flag_lookup_table[flag]
295    return flag_acc
296
297
298def _get_continue_on_failure(config):
299    continue_on_failure = config.getvalue("doctest_continue_on_failure")
300    if continue_on_failure:
301        # We need to turn off this if we use pdb since we should stop at
302        # the first failure
303        if config.getvalue("usepdb"):
304            continue_on_failure = False
305    return continue_on_failure
306
307
308class DoctestTextfile(pytest.Module):
309    obj = None
310
311    def collect(self):
312        import doctest
313
314        # inspired by doctest.testfile; ideally we would use it directly,
315        # but it doesn't support passing a custom checker
316        encoding = self.config.getini("doctest_encoding")
317        text = self.fspath.read_text(encoding)
318        filename = str(self.fspath)
319        name = self.fspath.basename
320        globs = {"__name__": "__main__"}
321
322        optionflags = get_optionflags(self)
323
324        runner = _get_runner(
325            verbose=0,
326            optionflags=optionflags,
327            checker=_get_checker(),
328            continue_on_failure=_get_continue_on_failure(self.config),
329        )
330        _fix_spoof_python2(runner, encoding)
331
332        parser = doctest.DocTestParser()
333        test = parser.get_doctest(text, globs, name, filename, 0)
334        if test.examples:
335            yield DoctestItem(test.name, self, runner, test)
336
337
338def _check_all_skipped(test):
339    """raises pytest.skip() if all examples in the given DocTest have the SKIP
340    option set.
341    """
342    import doctest
343
344    all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
345    if all_skipped:
346        pytest.skip("all tests skipped by +SKIP option")
347
348
349class DoctestModule(pytest.Module):
350
351    def collect(self):
352        import doctest
353
354        if self.fspath.basename == "conftest.py":
355            module = self.config.pluginmanager._importconftest(self.fspath)
356        else:
357            try:
358                module = self.fspath.pyimport()
359            except ImportError:
360                if self.config.getvalue("doctest_ignore_import_errors"):
361                    pytest.skip("unable to import module %r" % self.fspath)
362                else:
363                    raise
364        # uses internal doctest module parsing mechanism
365        finder = doctest.DocTestFinder()
366        optionflags = get_optionflags(self)
367        runner = _get_runner(
368            verbose=0,
369            optionflags=optionflags,
370            checker=_get_checker(),
371            continue_on_failure=_get_continue_on_failure(self.config),
372        )
373
374        for test in finder.find(module, module.__name__):
375            if test.examples:  # skip empty doctests
376                yield DoctestItem(test.name, self, runner, test)
377
378
379def _setup_fixtures(doctest_item):
380    """
381    Used by DoctestTextfile and DoctestItem to setup fixture information.
382    """
383
384    def func():
385        pass
386
387    doctest_item.funcargs = {}
388    fm = doctest_item.session._fixturemanager
389    doctest_item._fixtureinfo = fm.getfixtureinfo(
390        node=doctest_item, func=func, cls=None, funcargs=False
391    )
392    fixture_request = FixtureRequest(doctest_item)
393    fixture_request._fillfixtures()
394    return fixture_request
395
396
397def _get_checker():
398    """
399    Returns a doctest.OutputChecker subclass that takes in account the
400    ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES
401    to strip b'' prefixes.
402    Useful when the same doctest should run in Python 2 and Python 3.
403
404    An inner class is used to avoid importing "doctest" at the module
405    level.
406    """
407    if hasattr(_get_checker, "LiteralsOutputChecker"):
408        return _get_checker.LiteralsOutputChecker()
409
410    import doctest
411    import re
412
413    class LiteralsOutputChecker(doctest.OutputChecker):
414        """
415        Copied from doctest_nose_plugin.py from the nltk project:
416            https://github.com/nltk/nltk
417
418        Further extended to also support byte literals.
419        """
420
421        _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
422        _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
423
424        def check_output(self, want, got, optionflags):
425            res = doctest.OutputChecker.check_output(self, want, got, optionflags)
426            if res:
427                return True
428
429            allow_unicode = optionflags & _get_allow_unicode_flag()
430            allow_bytes = optionflags & _get_allow_bytes_flag()
431            if not allow_unicode and not allow_bytes:
432                return False
433
434            else:  # pragma: no cover
435
436                def remove_prefixes(regex, txt):
437                    return re.sub(regex, r"\1\2", txt)
438
439                if allow_unicode:
440                    want = remove_prefixes(self._unicode_literal_re, want)
441                    got = remove_prefixes(self._unicode_literal_re, got)
442                if allow_bytes:
443                    want = remove_prefixes(self._bytes_literal_re, want)
444                    got = remove_prefixes(self._bytes_literal_re, got)
445                res = doctest.OutputChecker.check_output(self, want, got, optionflags)
446                return res
447
448    _get_checker.LiteralsOutputChecker = LiteralsOutputChecker
449    return _get_checker.LiteralsOutputChecker()
450
451
452def _get_allow_unicode_flag():
453    """
454    Registers and returns the ALLOW_UNICODE flag.
455    """
456    import doctest
457
458    return doctest.register_optionflag("ALLOW_UNICODE")
459
460
461def _get_allow_bytes_flag():
462    """
463    Registers and returns the ALLOW_BYTES flag.
464    """
465    import doctest
466
467    return doctest.register_optionflag("ALLOW_BYTES")
468
469
470def _get_report_choice(key):
471    """
472    This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid
473    importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests.
474    """
475    import doctest
476
477    return {
478        DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
479        DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
480        DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
481        DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
482        DOCTEST_REPORT_CHOICE_NONE: 0,
483    }[
484        key
485    ]
486
487
488def _fix_spoof_python2(runner, encoding):
489    """
490    Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output. This
491    should patch only doctests for text files because they don't have a way to declare their
492    encoding. Doctests in docstrings from Python modules don't have the same problem given that
493    Python already decoded the strings.
494
495    This fixes the problem related in issue #2434.
496    """
497    from _pytest.compat import _PY2
498
499    if not _PY2:
500        return
501
502    from doctest import _SpoofOut
503
504    class UnicodeSpoof(_SpoofOut):
505
506        def getvalue(self):
507            result = _SpoofOut.getvalue(self)
508            if encoding and isinstance(result, bytes):
509                result = result.decode(encoding)
510            return result
511
512    runner._fakeout = UnicodeSpoof()
513
514
515@pytest.fixture(scope="session")
516def doctest_namespace():
517    """
518    Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests.
519    """
520    return dict()
521