PageRenderTime 39ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/Tests/test_Tutorial.py

http://github.com/biopython/biopython
Python | 299 lines | 224 code | 15 blank | 60 comment | 14 complexity | 586bdc8ff7b1e1cb4b482d34b7fff4d0 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. # Copyright 2011-2016 by Peter Cock. All rights reserved.
  2. # This code is part of the Biopython distribution and governed by its
  3. # license. Please see the LICENSE file that should have been included
  4. # as part of this package.
  5. #
  6. # This script looks for entries in the LaTeX source for the
  7. # Biopython Tutorial which can be turned into Python doctests,
  8. # e.g.
  9. #
  10. # %doctest
  11. # \begin{minted}{pycon}
  12. # >>> from Bio.Alphabet import generic_dna
  13. # >>> from Bio.Seq import Seq
  14. # >>> len("ACGT")
  15. # 4
  16. # \end{minted}
  17. #
  18. # Code snippets can be extended using a similar syntax, which
  19. # will create a single combined doctest:
  20. #
  21. # %cont-doctest
  22. # \begin{minted}{pycon}
  23. # >>> Seq("ACGT") == Seq("ACGT", generic_dna)
  24. # True
  25. # \end{minted}
  26. #
  27. # The %doctest line also supports a relative working directory,
  28. # and listing multiple Python dependencies as lib:XXX which will
  29. # ensure "import XXX" works before using the test. e.g.
  30. #
  31. # %doctest examples lib:numpy lib:scipy
  32. #
  33. # Additionally after the path, special keyword 'internet' is
  34. # used to flag online tests.
  35. #
  36. # Note if using lib:XXX or special value 'internet' you must
  37. # include a relative path to the working directory, just use '.'
  38. # for the default path, e.g.
  39. #
  40. # %doctest . lib:reportlab
  41. #
  42. # %doctest . internet
  43. #
  44. # TODO: Adding bin:XXX for checking binary XXX is on $PATH?
  45. #
  46. # See also "Writing doctests in the Tutorial" in the Tutorial
  47. # itself.
  48. # This future import will apply to all the doctests too:
  49. """Tests for Tutorial module."""
  50. import unittest
  51. import doctest
  52. import os
  53. import sys
  54. import warnings
  55. from lib2to3 import refactor
  56. from lib2to3.pgen2.tokenize import TokenError
  57. from Bio import BiopythonExperimentalWarning, MissingExternalDependencyError
  58. # This is the same mechanism used for run_tests.py --offline
  59. # to skip tests requiring the network.
  60. import requires_internet
  61. try:
  62. requires_internet.check()
  63. online = True
  64. except MissingExternalDependencyError:
  65. online = False
  66. if "--offline" in sys.argv:
  67. # Allow manual override via "python test_Tutorial.py --offline"
  68. online = False
  69. warnings.simplefilter("ignore", BiopythonExperimentalWarning)
  70. fixers = refactor.get_fixers_from_package("lib2to3.fixes")
  71. fixers.remove("lib2to3.fixes.fix_print") # Already using print function
  72. rt = refactor.RefactoringTool(fixers)
  73. assert rt.refactor_docstring(">>> print(2+2)\n4\n", "example1") == ">>> print(2+2)\n4\n"
  74. assert (
  75. rt.refactor_docstring(
  76. '>>> print("Two plus two is", 2+2)\nTwo plus two is 4\n', "example2"
  77. )
  78. == '>>> print("Two plus two is", 2+2)\nTwo plus two is 4\n'
  79. )
  80. # Cache this to restore the cwd at the end of the tests
  81. original_path = os.path.abspath(".")
  82. if os.path.basename(sys.argv[0]) == "test_Tutorial.py":
  83. # sys.argv[0] will be (relative) path to test_Turorial.py - use this to allow, e.g.
  84. # [base]$ python Tests/test_Tutorial.py
  85. # [Tests/]$ python test_Tutorial.py
  86. tutorial_base = os.path.abspath(
  87. os.path.join(os.path.dirname(sys.argv[0]), "../Doc/")
  88. )
  89. tutorial = os.path.join(tutorial_base, "Tutorial.tex")
  90. else:
  91. # Probably called via run_tests.py so current directory should (now) be Tests/
  92. # but may have been changed by run_tests.py so can't infer from sys.argv[0] with e.g.
  93. # [base]$ python Tests/run_tests.py test_Tutorial
  94. tutorial_base = os.path.abspath("../Doc/")
  95. tutorial = os.path.join(tutorial_base, "Tutorial.tex")
  96. if not os.path.isfile(tutorial):
  97. from Bio import MissingExternalDependencyError
  98. raise MissingExternalDependencyError("Could not find ../Doc/Tutorial.tex file")
  99. # Build a list of all the Tutorial LaTeX files:
  100. files = [tutorial]
  101. for latex in os.listdir(os.path.join(tutorial_base, "Tutorial/")):
  102. if latex.startswith("chapter_") and latex.endswith(".tex"):
  103. files.append(os.path.join(tutorial_base, "Tutorial", latex))
  104. def _extract(handle):
  105. line = handle.readline()
  106. if line != "\\begin{minted}{pycon}\n":
  107. raise ValueError(
  108. "Any '%doctest' or '%cont-doctest' line should be followed by '\\begin{minted}{pycon}'"
  109. )
  110. lines = []
  111. while True:
  112. line = handle.readline()
  113. if not line:
  114. if lines:
  115. print("".join(lines[:30]))
  116. raise ValueError("Didn't find end of test starting: %r", lines[0])
  117. else:
  118. raise ValueError("Didn't find end of test!")
  119. elif line.startswith("\\end{minted}"):
  120. break
  121. else:
  122. lines.append(line)
  123. return lines
  124. def extract_doctests(latex_filename):
  125. """Scan LaTeX file and pull out marked doctests as strings.
  126. This is a generator, yielding one tuple per doctest.
  127. """
  128. base_name = os.path.splitext(os.path.basename(latex_filename))[0]
  129. deps = ""
  130. folder = ""
  131. with open(latex_filename) as handle:
  132. line_number = 0
  133. lines = []
  134. name = None
  135. while True:
  136. line = handle.readline()
  137. line_number += 1
  138. if not line:
  139. # End of file
  140. break
  141. elif line.startswith("%cont-doctest"):
  142. x = _extract(handle)
  143. lines.extend(x)
  144. line_number += len(x) + 2
  145. elif line.startswith("%doctest"):
  146. if lines:
  147. if not lines[0].startswith(">>> "):
  148. raise ValueError("Should start '>>> ' not %r" % lines[0])
  149. yield name, "".join(lines), folder, deps
  150. lines = []
  151. deps = [x.strip() for x in line.split()[1:]]
  152. if deps:
  153. folder = deps[0]
  154. deps = deps[1:]
  155. else:
  156. folder = ""
  157. name = "test_%s_line_%05i" % (base_name, line_number)
  158. x = _extract(handle)
  159. lines.extend(x)
  160. line_number += len(x) + 2
  161. if lines:
  162. if not lines[0].startswith(">>> "):
  163. raise ValueError("Should start '>>> ' not %r" % lines[0])
  164. yield name, "".join(lines), folder, deps
  165. # yield "dummy", ">>> 2 + 2\n5\n"
  166. class TutorialDocTestHolder:
  167. """Python doctests extracted from the Biopython Tutorial."""
  168. pass
  169. def check_deps(dependencies):
  170. """Check 'lib:XXX' and 'internet' dependencies are met."""
  171. missing = []
  172. for dep in dependencies:
  173. if dep == "internet":
  174. if not online:
  175. missing.append("internet")
  176. else:
  177. assert dep.startswith("lib:"), dep
  178. lib = dep[4:]
  179. try:
  180. tmp = __import__(lib)
  181. del tmp
  182. except ImportError:
  183. missing.append(lib)
  184. return missing
  185. # Create dummy methods on the object purely to hold doctests
  186. missing_deps = set()
  187. for latex in files:
  188. # print("Extracting doctests from %s" % latex)
  189. for name, example, folder, deps in extract_doctests(latex):
  190. missing = check_deps(deps)
  191. if missing:
  192. missing_deps.update(missing)
  193. continue
  194. try:
  195. example = rt.refactor_docstring(example, name)
  196. except TokenError:
  197. raise ValueError("Problem with %s:\n%s" % (name, example)) from None
  198. def funct(n, d, f):
  199. global tutorial_base
  200. method = lambda x: None # noqa: E731
  201. if f:
  202. p = os.path.join(tutorial_base, f)
  203. method.__doc__ = f"{n}\n\n>>> import os\n>>> os.chdir({p!r})\n{d}\n"
  204. else:
  205. method.__doc__ = "%s\n\n%s\n" % (n, d)
  206. method._folder = f
  207. return method
  208. setattr(
  209. TutorialDocTestHolder,
  210. "doctest_%s" % name.replace(" ", "_"),
  211. funct(name, example, folder),
  212. )
  213. del funct
  214. # This is a TestCase class so it is found by run_tests.py
  215. class TutorialTestCase(unittest.TestCase):
  216. """Python doctests extracted from the Biopython Tutorial."""
  217. # Single method to be invoked by run_tests.py
  218. def test_doctests(self):
  219. """Run tutorial doctests."""
  220. runner = doctest.DocTestRunner()
  221. failures = []
  222. for test in doctest.DocTestFinder().find(TutorialDocTestHolder):
  223. failed, success = runner.run(test)
  224. if failed:
  225. name = test.name
  226. assert name.startswith("TutorialDocTestHolder.doctest_")
  227. failures.append(name[30:])
  228. # raise ValueError("Tutorial doctest %s failed" % test.name[30:])
  229. if failures:
  230. raise ValueError(
  231. "%i Tutorial doctests failed: %s" % (len(failures), ", ".join(failures))
  232. )
  233. def tearDown(self):
  234. global original_path
  235. os.chdir(original_path)
  236. # files currently don't get created during test with python3.5 and pypy
  237. # remove files created from chapter_phylo.tex
  238. delete_phylo_tutorial = ["examples/tree1.nwk", "examples/other_trees.nwk"]
  239. for file in delete_phylo_tutorial:
  240. if os.path.exists(os.path.join(tutorial_base, file)):
  241. os.remove(os.path.join(tutorial_base, file))
  242. # remove files created from chapter_cluster.tex
  243. tutorial_cluster_base = os.path.abspath("../Tests/")
  244. delete_cluster_tutorial = [
  245. "Cluster/cyano_result.atr",
  246. "Cluster/cyano_result.cdt",
  247. "Cluster/cyano_result.gtr",
  248. "Cluster/cyano_result_K_A2.kag",
  249. "Cluster/cyano_result_K_G5.kgg",
  250. "Cluster/cyano_result_K_G5_A2.cdt",
  251. ]
  252. for file in delete_cluster_tutorial:
  253. if os.path.exists(os.path.join(tutorial_cluster_base, file)):
  254. os.remove(os.path.join(tutorial_cluster_base, file))
  255. # This is to run the doctests if the script is called directly:
  256. if __name__ == "__main__":
  257. if missing_deps:
  258. print("Skipping tests needing the following:")
  259. for dep in sorted(missing_deps):
  260. print(" - %s" % dep)
  261. print("Running Tutorial doctests...")
  262. tests = doctest.testmod()
  263. if tests.failed:
  264. raise RuntimeError("%i/%i tests failed" % tests)
  265. print("Tests done")