##// END OF EJS Templates
now with skip
Matthias Bussonnier -
Show More
@@ -1,182 +1,186 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
26 26 #-----------------------------------------------------------------------------
27 27 # Copyright (C) 2009-2011 The IPython Development Team
28 28 #
29 29 # Distributed under the terms of the BSD License. The full license is in
30 30 # the file COPYING, distributed as part of this software.
31 31 #-----------------------------------------------------------------------------
32 32
33 33 #-----------------------------------------------------------------------------
34 34 # Imports
35 35 #-----------------------------------------------------------------------------
36 36
37 37 # Stdlib
38 38 import re
39 39 import sys
40 40 import unittest
41 41 from doctest import DocTestFinder, DocTestRunner, TestResults
42 42 from IPython.terminal.interactiveshell import InteractiveShell
43 43
44 44 #-----------------------------------------------------------------------------
45 45 # Classes and functions
46 46 #-----------------------------------------------------------------------------
47 47
48 48 def count_failures(runner):
49 49 """Count number of failures in a doctest runner.
50 50
51 51 Code modeled after the summarize() method in doctest.
52 52 """
53 53 if sys.version_info < (3, 13):
54 54 return [TestResults(f, t) for f, t in runner._name2ft.values() if f > 0]
55 55 else:
56 return [TestResults(f, t) for f, t in runner._stats.values() if f > 0]
56 return [
57 TestResults(failure, try_)
58 for failure, try_, skip in runner._stats.values()
59 if f > 0
60 ]
57 61
58 62
59 63 class IPython2PythonConverter(object):
60 64 """Convert IPython 'syntax' to valid Python.
61 65
62 66 Eventually this code may grow to be the full IPython syntax conversion
63 67 implementation, but for now it only does prompt conversion."""
64 68
65 69 def __init__(self):
66 70 self.rps1 = re.compile(r'In\ \[\d+\]: ')
67 71 self.rps2 = re.compile(r'\ \ \ \.\.\.+: ')
68 72 self.rout = re.compile(r'Out\[\d+\]: \s*?\n?')
69 73 self.pyps1 = '>>> '
70 74 self.pyps2 = '... '
71 75 self.rpyps1 = re.compile (r'(\s*%s)(.*)$' % self.pyps1)
72 76 self.rpyps2 = re.compile (r'(\s*%s)(.*)$' % self.pyps2)
73 77
74 78 def __call__(self, ds):
75 79 """Convert IPython prompts to python ones in a string."""
76 80 from . import globalipapp
77 81
78 82 pyps1 = '>>> '
79 83 pyps2 = '... '
80 84 pyout = ''
81 85
82 86 dnew = ds
83 87 dnew = self.rps1.sub(pyps1, dnew)
84 88 dnew = self.rps2.sub(pyps2, dnew)
85 89 dnew = self.rout.sub(pyout, dnew)
86 90 ip = InteractiveShell.instance()
87 91
88 92 # Convert input IPython source into valid Python.
89 93 out = []
90 94 newline = out.append
91 95 for line in dnew.splitlines():
92 96
93 97 mps1 = self.rpyps1.match(line)
94 98 if mps1 is not None:
95 99 prompt, text = mps1.groups()
96 100 newline(prompt+ip.prefilter(text, False))
97 101 continue
98 102
99 103 mps2 = self.rpyps2.match(line)
100 104 if mps2 is not None:
101 105 prompt, text = mps2.groups()
102 106 newline(prompt+ip.prefilter(text, True))
103 107 continue
104 108
105 109 newline(line)
106 110 newline('') # ensure a closing newline, needed by doctest
107 111 #print "PYSRC:", '\n'.join(out) # dbg
108 112 return '\n'.join(out)
109 113
110 114 #return dnew
111 115
112 116
113 117 class Doc2UnitTester(object):
114 118 """Class whose instances act as a decorator for docstring testing.
115 119
116 120 In practice we're only likely to need one instance ever, made below (though
117 121 no attempt is made at turning it into a singleton, there is no need for
118 122 that).
119 123 """
120 124 def __init__(self, verbose=False):
121 125 """New decorator.
122 126
123 127 Parameters
124 128 ----------
125 129
126 130 verbose : boolean, optional (False)
127 131 Passed to the doctest finder and runner to control verbosity.
128 132 """
129 133 self.verbose = verbose
130 134 # We can reuse the same finder for all instances
131 135 self.finder = DocTestFinder(verbose=verbose, recurse=False)
132 136
133 137 def __call__(self, func):
134 138 """Use as a decorator: doctest a function's docstring as a unittest.
135 139
136 140 This version runs normal doctests, but the idea is to make it later run
137 141 ipython syntax instead."""
138 142
139 143 # Capture the enclosing instance with a different name, so the new
140 144 # class below can see it without confusion regarding its own 'self'
141 145 # that will point to the test instance at runtime
142 146 d2u = self
143 147
144 148 # Rewrite the function's docstring to have python syntax
145 149 if func.__doc__ is not None:
146 150 func.__doc__ = ip2py(func.__doc__)
147 151
148 152 # Now, create a tester object that is a real unittest instance, so
149 153 # normal unittest machinery (or Nose, or Trial) can find it.
150 154 class Tester(unittest.TestCase):
151 155 def test(self):
152 156 # Make a new runner per function to be tested
153 157 runner = DocTestRunner(verbose=d2u.verbose)
154 158 for the_test in d2u.finder.find(func, func.__name__):
155 159 runner.run(the_test)
156 160 failed = count_failures(runner)
157 161 if failed:
158 162 # Since we only looked at a single function's docstring,
159 163 # failed should contain at most one item. More than that
160 164 # is a case we can't handle and should error out on
161 165 if len(failed) > 1:
162 166 err = "Invalid number of test results: %s" % failed
163 167 raise ValueError(err)
164 168 # Report a normal failure.
165 169 self.fail('failed doctests: %s' % str(failed[0]))
166 170
167 171 # Rename it so test reports have the original signature.
168 172 Tester.__name__ = func.__name__
169 173 return Tester
170 174
171 175
172 176 def ipdocstring(func):
173 177 """Change the function docstring via ip2py.
174 178 """
175 179 if func.__doc__ is not None:
176 180 func.__doc__ = ip2py(func.__doc__)
177 181 return func
178 182
179 183
180 184 # Make an instance of the classes for public use
181 185 ipdoctest = Doc2UnitTester()
182 186 ip2py = IPython2PythonConverter()
General Comments 0
You need to be logged in to leave comments. Login now