1""" discover and run doctests in modules and test files.""" 2from __future__ import absolute_import 3 4import traceback 5 6import pytest 7from _pytest._code.code import TerminalRepr, ReprFileLocation, ExceptionInfo 8from _pytest.python import FixtureRequest 9 10 11 12def pytest_addoption(parser): 13 parser.addini('doctest_optionflags', 'option flags for doctests', 14 type="args", default=["ELLIPSIS"]) 15 group = parser.getgroup("collect") 16 group.addoption("--doctest-modules", 17 action="store_true", default=False, 18 help="run doctests in all .py modules", 19 dest="doctestmodules") 20 group.addoption("--doctest-glob", 21 action="append", default=[], metavar="pat", 22 help="doctests file matching pattern, default: test*.txt", 23 dest="doctestglob") 24 group.addoption("--doctest-ignore-import-errors", 25 action="store_true", default=False, 26 help="ignore doctest ImportErrors", 27 dest="doctest_ignore_import_errors") 28 29 30def pytest_collect_file(path, parent): 31 config = parent.config 32 if path.ext == ".py": 33 if config.option.doctestmodules: 34 return DoctestModule(path, parent) 35 elif _is_doctest(config, path, parent): 36 return DoctestTextfile(path, parent) 37 38 39def _is_doctest(config, path, parent): 40 if path.ext in ('.txt', '.rst') and parent.session.isinitpath(path): 41 return True 42 globs = config.getoption("doctestglob") or ['test*.txt'] 43 for glob in globs: 44 if path.check(fnmatch=glob): 45 return True 46 return False 47 48 49class ReprFailDoctest(TerminalRepr): 50 51 def __init__(self, reprlocation, lines): 52 self.reprlocation = reprlocation 53 self.lines = lines 54 55 def toterminal(self, tw): 56 for line in self.lines: 57 tw.line(line) 58 self.reprlocation.toterminal(tw) 59 60 61class DoctestItem(pytest.Item): 62 63 def __init__(self, name, parent, runner=None, dtest=None): 64 super(DoctestItem, self).__init__(name, parent) 65 self.runner = runner 66 self.dtest = dtest 67 self.obj = None 68 self.fixture_request = None 69 70 def setup(self): 71 if self.dtest is not None: 72 self.fixture_request = _setup_fixtures(self) 73 globs = dict(getfixture=self.fixture_request.getfuncargvalue) 74 self.dtest.globs.update(globs) 75 76 def runtest(self): 77 _check_all_skipped(self.dtest) 78 self.runner.run(self.dtest) 79 80 def repr_failure(self, excinfo): 81 import doctest 82 if excinfo.errisinstance((doctest.DocTestFailure, 83 doctest.UnexpectedException)): 84 doctestfailure = excinfo.value 85 example = doctestfailure.example 86 test = doctestfailure.test 87 filename = test.filename 88 if test.lineno is None: 89 lineno = None 90 else: 91 lineno = test.lineno + example.lineno + 1 92 message = excinfo.type.__name__ 93 reprlocation = ReprFileLocation(filename, lineno, message) 94 checker = _get_checker() 95 REPORT_UDIFF = doctest.REPORT_UDIFF 96 if lineno is not None: 97 lines = doctestfailure.test.docstring.splitlines(False) 98 # add line numbers to the left of the error message 99 lines = ["%03d %s" % (i + test.lineno + 1, x) 100 for (i, x) in enumerate(lines)] 101 # trim docstring error lines to 10 102 lines = lines[example.lineno - 9:example.lineno + 1] 103 else: 104 lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example'] 105 indent = '>>>' 106 for line in example.source.splitlines(): 107 lines.append('??? %s %s' % (indent, line)) 108 indent = '...' 109 if excinfo.errisinstance(doctest.DocTestFailure): 110 lines += checker.output_difference(example, 111 doctestfailure.got, REPORT_UDIFF).split("\n") 112 else: 113 inner_excinfo = ExceptionInfo(excinfo.value.exc_info) 114 lines += ["UNEXPECTED EXCEPTION: %s" % 115 repr(inner_excinfo.value)] 116 lines += traceback.format_exception(*excinfo.value.exc_info) 117 return ReprFailDoctest(reprlocation, lines) 118 else: 119 return super(DoctestItem, self).repr_failure(excinfo) 120 121 def reportinfo(self): 122 return self.fspath, None, "[doctest] %s" % self.name 123 124 125def _get_flag_lookup(): 126 import doctest 127 return dict(DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1, 128 DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE, 129 NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE, 130 ELLIPSIS=doctest.ELLIPSIS, 131 IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL, 132 COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, 133 ALLOW_UNICODE=_get_allow_unicode_flag(), 134 ALLOW_BYTES=_get_allow_bytes_flag(), 135 ) 136 137 138def get_optionflags(parent): 139 optionflags_str = parent.config.getini("doctest_optionflags") 140 flag_lookup_table = _get_flag_lookup() 141 flag_acc = 0 142 for flag in optionflags_str: 143 flag_acc |= flag_lookup_table[flag] 144 return flag_acc 145 146 147class DoctestTextfile(DoctestItem, pytest.Module): 148 149 def runtest(self): 150 import doctest 151 fixture_request = _setup_fixtures(self) 152 153 # inspired by doctest.testfile; ideally we would use it directly, 154 # but it doesn't support passing a custom checker 155 text = self.fspath.read() 156 filename = str(self.fspath) 157 name = self.fspath.basename 158 globs = dict(getfixture=fixture_request.getfuncargvalue) 159 if '__name__' not in globs: 160 globs['__name__'] = '__main__' 161 162 optionflags = get_optionflags(self) 163 runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, 164 checker=_get_checker()) 165 166 parser = doctest.DocTestParser() 167 test = parser.get_doctest(text, globs, name, filename, 0) 168 _check_all_skipped(test) 169 runner.run(test) 170 171 172def _check_all_skipped(test): 173 """raises pytest.skip() if all examples in the given DocTest have the SKIP 174 option set. 175 """ 176 import doctest 177 all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples) 178 if all_skipped: 179 pytest.skip('all tests skipped by +SKIP option') 180 181 182class DoctestModule(pytest.Module): 183 def collect(self): 184 import doctest 185 if self.fspath.basename == "conftest.py": 186 module = self.config.pluginmanager._importconftest(self.fspath) 187 else: 188 try: 189 module = self.fspath.pyimport() 190 except ImportError: 191 if self.config.getvalue('doctest_ignore_import_errors'): 192 pytest.skip('unable to import module %r' % self.fspath) 193 else: 194 raise 195 # uses internal doctest module parsing mechanism 196 finder = doctest.DocTestFinder() 197 optionflags = get_optionflags(self) 198 runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, 199 checker=_get_checker()) 200 for test in finder.find(module, module.__name__): 201 if test.examples: # skip empty doctests 202 yield DoctestItem(test.name, self, runner, test) 203 204 205def _setup_fixtures(doctest_item): 206 """ 207 Used by DoctestTextfile and DoctestItem to setup fixture information. 208 """ 209 def func(): 210 pass 211 212 doctest_item.funcargs = {} 213 fm = doctest_item.session._fixturemanager 214 doctest_item._fixtureinfo = fm.getfixtureinfo(node=doctest_item, func=func, 215 cls=None, funcargs=False) 216 fixture_request = FixtureRequest(doctest_item) 217 fixture_request._fillfixtures() 218 return fixture_request 219 220 221def _get_checker(): 222 """ 223 Returns a doctest.OutputChecker subclass that takes in account the 224 ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES 225 to strip b'' prefixes. 226 Useful when the same doctest should run in Python 2 and Python 3. 227 228 An inner class is used to avoid importing "doctest" at the module 229 level. 230 """ 231 if hasattr(_get_checker, 'LiteralsOutputChecker'): 232 return _get_checker.LiteralsOutputChecker() 233 234 import doctest 235 import re 236 237 class LiteralsOutputChecker(doctest.OutputChecker): 238 """ 239 Copied from doctest_nose_plugin.py from the nltk project: 240 https://github.com/nltk/nltk 241 242 Further extended to also support byte literals. 243 """ 244 245 _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) 246 _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE) 247 248 def check_output(self, want, got, optionflags): 249 res = doctest.OutputChecker.check_output(self, want, got, 250 optionflags) 251 if res: 252 return True 253 254 allow_unicode = optionflags & _get_allow_unicode_flag() 255 allow_bytes = optionflags & _get_allow_bytes_flag() 256 if not allow_unicode and not allow_bytes: 257 return False 258 259 else: # pragma: no cover 260 def remove_prefixes(regex, txt): 261 return re.sub(regex, r'\1\2', txt) 262 263 if allow_unicode: 264 want = remove_prefixes(self._unicode_literal_re, want) 265 got = remove_prefixes(self._unicode_literal_re, got) 266 if allow_bytes: 267 want = remove_prefixes(self._bytes_literal_re, want) 268 got = remove_prefixes(self._bytes_literal_re, got) 269 res = doctest.OutputChecker.check_output(self, want, got, 270 optionflags) 271 return res 272 273 _get_checker.LiteralsOutputChecker = LiteralsOutputChecker 274 return _get_checker.LiteralsOutputChecker() 275 276 277def _get_allow_unicode_flag(): 278 """ 279 Registers and returns the ALLOW_UNICODE flag. 280 """ 281 import doctest 282 return doctest.register_optionflag('ALLOW_UNICODE') 283 284 285def _get_allow_bytes_flag(): 286 """ 287 Registers and returns the ALLOW_BYTES flag. 288 """ 289 import doctest 290 return doctest.register_optionflag('ALLOW_BYTES') 291