ipunittest.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. """Experimental code for cleaner support of IPython syntax with unittest.
  2. In IPython up until 0.10, we've used very hacked up nose machinery for running
  3. tests with IPython special syntax, and this has proved to be extremely slow.
  4. This module provides decorators to try a different approach, stemming from a
  5. conversation Brian and I (FP) had about this problem Sept/09.
  6. The goal is to be able to easily write simple functions that can be seen by
  7. unittest as tests, and ultimately for these to support doctests with full
  8. IPython syntax. Nose already offers this based on naming conventions and our
  9. hackish plugins, but we are seeking to move away from nose dependencies if
  10. possible.
  11. This module follows a different approach, based on decorators.
  12. - A decorator called @ipdoctest can mark any function as having a docstring
  13. that should be viewed as a doctest, but after syntax conversion.
  14. Authors
  15. -------
  16. - Fernando Perez <Fernando.Perez@berkeley.edu>
  17. """
  18. #-----------------------------------------------------------------------------
  19. # Copyright (C) 2009-2011 The IPython Development Team
  20. #
  21. # Distributed under the terms of the BSD License. The full license is in
  22. # the file COPYING, distributed as part of this software.
  23. #-----------------------------------------------------------------------------
  24. #-----------------------------------------------------------------------------
  25. # Imports
  26. #-----------------------------------------------------------------------------
  27. # Stdlib
  28. import re
  29. import sys
  30. import unittest
  31. import builtins
  32. from doctest import DocTestFinder, DocTestRunner, TestResults
  33. from IPython.terminal.interactiveshell import InteractiveShell
  34. #-----------------------------------------------------------------------------
  35. # Classes and functions
  36. #-----------------------------------------------------------------------------
  37. def count_failures(runner):
  38. """Count number of failures in a doctest runner.
  39. Code modeled after the summarize() method in doctest.
  40. """
  41. if sys.version_info < (3, 13):
  42. return [TestResults(f, t) for f, t in runner._name2ft.values() if f > 0]
  43. else:
  44. return [
  45. TestResults(failure, try_)
  46. for failure, try_, skip in runner._stats.values()
  47. if failure > 0
  48. ]
  49. class IPython2PythonConverter:
  50. """Convert IPython 'syntax' to valid Python.
  51. Eventually this code may grow to be the full IPython syntax conversion
  52. implementation, but for now it only does prompt conversion."""
  53. def __init__(self):
  54. self.rps1 = re.compile(r'In\ \[\d+\]: ')
  55. self.rps2 = re.compile(r'\ \ \ \.\.\.+: ')
  56. self.rout = re.compile(r'Out\[\d+\]: \s*?\n?')
  57. self.pyps1 = '>>> '
  58. self.pyps2 = '... '
  59. self.rpyps1 = re.compile (r'(\s*%s)(.*)$' % self.pyps1)
  60. self.rpyps2 = re.compile (r'(\s*%s)(.*)$' % self.pyps2)
  61. def __call__(self, ds):
  62. """Convert IPython prompts to python ones in a string."""
  63. from . import globalipapp
  64. pyps1 = '>>> '
  65. pyps2 = '... '
  66. pyout = ''
  67. dnew = ds
  68. dnew = self.rps1.sub(pyps1, dnew)
  69. dnew = self.rps2.sub(pyps2, dnew)
  70. dnew = self.rout.sub(pyout, dnew)
  71. ip = InteractiveShell.instance()
  72. # Convert input IPython source into valid Python.
  73. out = []
  74. newline = out.append
  75. for line in dnew.splitlines():
  76. mps1 = self.rpyps1.match(line)
  77. if mps1 is not None:
  78. prompt, text = mps1.groups()
  79. newline(prompt+ip.prefilter(text, False))
  80. continue
  81. mps2 = self.rpyps2.match(line)
  82. if mps2 is not None:
  83. prompt, text = mps2.groups()
  84. newline(prompt+ip.prefilter(text, True))
  85. continue
  86. newline(line)
  87. newline('') # ensure a closing newline, needed by doctest
  88. # print("PYSRC:", '\n'.join(out)) # dbg
  89. return '\n'.join(out)
  90. #return dnew
  91. class Doc2UnitTester:
  92. """Class whose instances act as a decorator for docstring testing.
  93. In practice we're only likely to need one instance ever, made below (though
  94. no attempt is made at turning it into a singleton, there is no need for
  95. that).
  96. """
  97. def __init__(self, verbose: bool=False):
  98. """New decorator.
  99. Parameters
  100. ----------
  101. verbose : boolean, optional (False)
  102. Passed to the doctest finder and runner to control verbosity.
  103. """
  104. self.verbose = verbose
  105. # We can reuse the same finder for all instances
  106. self.finder = DocTestFinder(verbose=verbose, recurse=False)
  107. def __call__(self, func):
  108. """Use as a decorator: doctest a function's docstring as a unittest.
  109. This version runs normal doctests, but the idea is to make it later run
  110. ipython syntax instead."""
  111. # Capture the enclosing instance with a different name, so the new
  112. # class below can see it without confusion regarding its own 'self'
  113. # that will point to the test instance at runtime
  114. d2u = self
  115. # Rewrite the function's docstring to have python syntax
  116. if func.__doc__ is not None:
  117. func.__doc__ = ip2py(func.__doc__)
  118. # Now, create a tester object that is a real unittest instance, so
  119. # normal unittest machinery (or Nose, or Trial) can find it.
  120. class Tester(unittest.TestCase):
  121. def test(self):
  122. # Make a new runner per function to be tested
  123. runner = DocTestRunner(verbose=d2u.verbose)
  124. for the_test in d2u.finder.find(func, func.__name__):
  125. runner.run(the_test)
  126. failed = count_failures(runner)
  127. if failed:
  128. # Since we only looked at a single function's docstring,
  129. # failed should contain at most one item. More than that
  130. # is a case we can't handle and should error out on
  131. if len(failed) > 1:
  132. err = "Invalid number of test results: %s" % failed
  133. raise ValueError(err)
  134. # Report a normal failure.
  135. self.fail('failed doctests: %s' % str(failed[0]))
  136. # Rename it so test reports have the original signature.
  137. Tester.__name__ = func.__name__
  138. return Tester
  139. def ipdocstring(func):
  140. """Change the function docstring via ip2py.
  141. """
  142. if func.__doc__ is not None:
  143. func.__doc__ = ip2py(func.__doc__)
  144. return func
  145. # Make an instance of the classes for public use
  146. ipdoctest = Doc2UnitTester()
  147. ip2py = IPython2PythonConverter()