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