File: doctest.py

package info (click to toggle)
python-sybil 9.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,148 kB
  • sloc: python: 4,510; makefile: 90
file content (140 lines) | stat: -rw-r--r-- 4,606 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import re

import doctest
from doctest import set_unittest_reportflags
from typing import Any, Dict, List, Optional

from sybil import Example

#: A doctest option flag to limit comparison of numbers in expected output to the precision
#: specified in the example.
NUMBER = doctest.register_optionflag("NUMBER")


class DocTest(doctest.DocTest):
    def __init__(
            self,
            examples: List[doctest.Example],
            globs: Dict[str, Any],
            name: str,
            filename: Optional[str],
            lineno: Optional[int],
            docstring: Optional[str],
        ) -> None:
        # do everything like regular doctests, but don't make a copy of globs
        doctest.DocTest.__init__(self, examples, globs, name, filename, lineno, docstring)
        self.globs = globs


def float_approx_equal(expected: str, actual: str, tolerance: float=1e-12) -> bool:
    return abs(float(expected) - float(actual)) <= tolerance


class OutputChecker(doctest.OutputChecker):
    _number_re = re.compile(
        r"""
        (?P<number>
          (?P<mantissa>
            (?P<integer1> [+-]?\d*)\.(?P<fraction>\d+)
            |
            (?P<integer2> [+-]?\d+)\.
          )
          (?:
            [Ee]
            (?P<exponent1> [+-]?\d+)
          )?
          |
          (?P<integer3> [+-]?\d+)
          (?:
            [Ee]
            (?P<exponent2> [+-]?\d+)
          )
        )
        """,
        re.VERBOSE,
    )

    def _remove_unwanted_precision(self, want: str, got: str) -> str:
        wants = list(self._number_re.finditer(want))
        gots = list(self._number_re.finditer(got))
        if len(wants) != len(gots):
            return got
        offset = 0
        for w, g in zip(wants, gots):
            fraction: Optional[str] = w.group("fraction")
            exponent: Optional[str] = w.group("exponent1")
            if exponent is None:
                exponent = w.group("exponent2")
            if fraction is None:
                precision = 0
            else:
                precision = len(fraction)
            if exponent is not None:
                precision -= int(exponent)
            if float_approx_equal(w.group(), g.group(), tolerance=10 ** -precision):
                # They're close enough. Replace the text we actually
                # got with the text we want, so that it will match when we
                # check the string literally.
                got = (
                        got[: g.start() + offset] + w.group() + got[g.end() + offset:]
                )
                offset += w.end() - w.start() - (g.end() - g.start())
        return got

    def check_output(self, want: str, got: str, optionflags: int) -> bool:
        allow_number = optionflags & NUMBER
        if allow_number:
            got = self._remove_unwanted_precision(want, got)
        return doctest.OutputChecker.check_output(self, want, got, optionflags)


class DocTestRunner(doctest.DocTestRunner):

    def __init__(self, optionflags: int) -> None:
        _unittest_reportflags = set_unittest_reportflags(0)
        set_unittest_reportflags(_unittest_reportflags)
        optionflags |= _unittest_reportflags
        doctest.DocTestRunner.__init__(
            self,
            verbose=False,
            optionflags=optionflags,
            checker=OutputChecker(),
        )

    def _failure_header(self, test: DocTest, example: doctest.Example) -> str:
        return ''


class DocTestEvaluator:
    """
    The :any:`Evaluator` to use for :class:`Regions <sybil.Region>` yielded by
    a :class:`~sybil.parsers.abstract.doctest.DocTestStringParser`.


    :param optionflags:
        :ref:`doctest option flags<option-flags-and-directives>` to use
        when evaluating examples.
    """

    def __init__(self, optionflags: int = 0) -> None:
        self.runner = DocTestRunner(optionflags)

    def __call__(self, sybil_example: Example) -> str:
        example = sybil_example.parsed
        namespace = sybil_example.namespace
        output: List[str] = []
        remove_name = False
        try:
            if '__name__' not in namespace:
                remove_name = True
                namespace['__name__'] = '__test__'
            self.runner.run(
                DocTest([example], namespace, name=sybil_example.path,
                        filename=None, lineno=example.lineno, docstring=None),
                clear_globs=False,
                out=output.append
            )
        finally:
            if remove_name:
                del namespace['__name__']
        return ''.join(output)