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