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