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