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