doctest.py 10 KB

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