doctest.py 17 KB

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