ApiTest.check_sugg_added()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
rs 9.4285
cc 2
1
# -*- coding: utf-8
2
"""Unit tests for didyoumean APIs."""
3
from didyoumean_api import didyoumean_decorator, didyoumean_contextmanager,\
4
    didyoumean_postmortem, didyoumean_enablehook, didyoumean_disablehook
5
from didyoumean_common_tests import TestWithStringFunction,\
6
    get_exception, no_exception, NoFileIoError
7
import unittest2
8
import sys
9
import os
10
11
12
class ApiTest(TestWithStringFunction):
13
    """Tests about the didyoumean APIs."""
14
15
    def run_with_api(self, code):
16
        """Abstract method to run code with tested API."""
17
        raise NotImplementedError("'run_with_api' needs to be implemented")
18
19
    def get_exc_with_api(self, code):
20
        """Get exception raised with running code with tested API."""
21
        try:
22
            self.run_with_api(code)
23
        except:
24
            return sys.exc_info()
25
        assert False, "No exception thrown"
26
27
    def get_exc_as_str(self, code, type_arg):
28
        """Retrieve string representations of exceptions raised by code.
29
30
        String representations are provided for the same code run
31
        with and without the API.
32
        """
33
        type1, value1, _ = get_exception(code)
34
        details1 = "%s %s" % (str(type1), str(value1))
35
        self.assertTrue(isinstance(value1, type1), details1)
36
        self.assertEqual(type_arg, type1, details1)
37
        str1, repr1 = str(value1), repr(value1)
38
        type2, value2, _ = self.get_exc_with_api(code)
39
        details2 = "%s %s" % (str(type2), str(value2))
40
        self.assertTrue(isinstance(value2, type2), details2)
41
        self.assertEqual(type_arg, type2, details2)
42
        str2, repr2 = str(value2), repr(value2)
43
        return (str1, repr1, str2, repr2)
44
45
    def check_sugg_added(self, code, type_, sugg, normalise_quotes=False):
46
        """Check that the suggestion gets added to the exception.
47
48
        Get the string representations for the exception before and after
49
        and check that the suggestion `sugg` is added to `before` to get
50
        `after`. `normalise_quotes` can be provided to replace all quotes
51
        by double quotes before checking the `repr()` representations as
52
        they may get changed sometimes.
53
        """
54
        str1, repr1, str2, repr2 = self.get_exc_as_str(
55
            code, type_)
56
        self.assertStringAdded(sugg, str1, str2, True)
57
        if normalise_quotes:
58
            sugg = sugg.replace("'", '"')
59
            repr1 = repr1.replace("'", '"')
60
            repr2 = repr2.replace("'", '"')
61
        self.assertStringAdded(sugg, repr1, repr2, True)
62
63
    def test_api_no_exception(self):
64
        """Check the case with no exception."""
65
        code = 'babar = 0\nbabar'
66
        no_exception(code)
67
        self.run_with_api(code)
68
69
    def test_api_suggestion(self):
70
        """Check the case with a suggestion."""
71
        type_ = NameError
72
        sugg = ". Did you mean 'babar' (local)?"
73
        code = 'babar = 0\nbaba'
74
        self.check_sugg_added(code, type_, sugg)
75
76
    def test_api_no_suggestion(self):
77
        """Check the case with no suggestion."""
78
        type_ = NameError
79
        sugg = ""
80
        code = 'babar = 0\nfdjhflsdsqfjlkqs'
81
        self.check_sugg_added(code, type_, sugg)
82
83
    def test_api_syntax(self):
84
        """Check the case with syntax error suggestion."""
85
        type_ = SyntaxError
86
        sugg = ". Did you mean to indent it, 'sys.exit([arg])'?"
87
        code = 'return'
88
        self.check_sugg_added(code, type_, sugg, True)
89
90
    def test_api_ioerror(self):
91
        """Check the case with IO error suggestion."""
92
        type_ = NoFileIoError
93
        home = os.path.expanduser("~")
94
        sugg = ". Did you mean '" + home + "' (calling os.path.expanduser)?"
95
        code = 'with open("~") as f:\n\tpass'
96
        self.check_sugg_added(code, type_, sugg, True)
97
98
99
class DecoratorTest(unittest2.TestCase, ApiTest):
100
    """Tests about the didyoumean decorator."""
101
102
    def run_with_api(self, code):
103
        """Run code with didyoumean decorator."""
104
        @didyoumean_decorator
105
        def my_func():
106
            no_exception(code)
107
        my_func()
108
109
110
class ContextManagerTest(unittest2.TestCase, ApiTest):
111
    """Tests about the didyoumean context manager."""
112
113
    def run_with_api(self, code):
114
        """Run code with didyoumean context manager."""
115
        with didyoumean_contextmanager():
116
            no_exception(code)
117
118
119
class PostMortemTest(unittest2.TestCase, ApiTest):
120
    """Tests about the didyoumean post mortem."""
121
122
    def run_with_api(self, code):
123
        """Run code with didyoumean post mortem."""
124
        # A bit of an ugly way to proceed, in real life scenario
125
        # the sys.last_<something> members are set automatically.
126
        for a in ('last_type', 'last_value', 'last_traceback'):
127
            if hasattr(sys, a):
128
                delattr(sys, a)
129
        try:
130
            no_exception(code)
131
        except:
132
            sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
133
        ret = didyoumean_postmortem()
134
        if ret is not None:
135
            raise ret
136
137
138
class HookTest(ApiTest):
139
    """Tests about the didyoumean hooks.
140
141
    These tests are somewhat artificial as one needs to explicitely catch
142
    the exception, simulate a call to the function that would have been
143
    called for an uncatched exception and reraise it (so that then it gets
144
    caught by yet another try-except).
145
    Realistically it might not catch any real-life problems (because these
146
    would happen when the shell does not behave as expected) but it might be
147
    useful to prevent regressions.
148
    """
149
150
    pass  # Can't write tests as the hook seems to be ignored.
151
152
153
class NotATest(object):
154
    """Dummy subclass to inherit from instead of unittest2.TestCase.
155
156
    The tests from ExceptHookTest are not very relevant most of the
157
    time and they flood the output because of the dodgy things we do
158
    with sys.excepthook. Most of the time, it is better not to run them
159
    but I still want to keep them for the time being. The solution is
160
    to be able to make this easily configurable by having the dependency
161
    over unittest2.TestCase optional with a simple test:
162
        class MyTest(unittest2.TestCase if <cond> else NotRunTest, ...)
163
    """
164
165
    pass
166
167
168
class ExceptHookTest(unittest2.TestCase if True else NotATest, HookTest):
169
    """Tests about the didyoumean excepthook."""
170
171
    def run_with_api(self, code):
172
        """Run code with didyoumean after enabling didyoumean hook."""
173
        prev_hook = sys.excepthook
174
        self.assertEqual(prev_hook, sys.excepthook)
175
        didyoumean_enablehook()
176
        self.assertNotEqual(prev_hook, sys.excepthook)
177
        try:
178
            no_exception(code)
179
        except:
180
            last_type, last_value, last_traceback = sys.exc_info()
181
            sys.excepthook(last_type, last_value, last_traceback)
182
            raise
183
        finally:
184
            self.assertNotEqual(prev_hook, sys.excepthook)
185
            didyoumean_disablehook()
186
            self.assertEqual(prev_hook, sys.excepthook)
187
188
189
class DummyShell:
190
    """Dummy class to emulate the iPython interactive shell.
191
192
    https://ipython.org/ipython-doc/dev/api/generated/IPython.core.interactiveshell.html
193
    """
194
195
    def __init__(self):
196
        """Init."""
197
        self.handler = None
198
        self.exc_tuple = None
199
200
    def set_custom_exc(self, exc_tuple, handler):
201
        """Emulate the interactiveshell.set_custom_exc method."""
202
        self.handler = handler
203
        self.exc_tuple = exc_tuple
204
205
    def showtraceback(self, exc_tuple=None,
206
                      filename=None, tb_offset=None, exception_only=False):
207
        """Emulate the interactiveshell.showtraceback method.
208
209
        Calls the custom exception handler if is it set.
210
        """
211
        if self.handler is not None and self.exc_tuple is not None:
212
            etype, evalue, tb = exc_tuple
213
            func, self.handler = self.handler, None  # prevent recursive calls
214
            func(self, etype, evalue, tb, tb_offset)
215
            self.handler = func
216
217
    def set(self, module):
218
        """Make shell accessible in module via 'get_ipython'."""
219
        assert 'get_ipython' not in dir(module)
220
        module.get_ipython = lambda: self
221
222
    def remove(self, module):
223
        """Make shell un-accessible in module via 'get_ipython'."""
224
        del module.get_ipython
225
226
227
class IPythonHookTest(unittest2.TestCase, HookTest):
228
    """Tests about the didyoumean custom exception handler for iPython.
229
230
    These tests need a dummy shell to be create to be able to use/define
231
    its functions related to the custom exception handlers.
232
    """
233
234
    def run_with_api(self, code):
235
        """Run code with didyoumean after enabling didyoumean hook."""
236
        prev_handler = None
237
        shell = DummyShell()
238
        module = sys.modules['didyoumean_api']
239
        shell.set(module)
240
        self.assertEqual(shell.handler, prev_handler)
241
        didyoumean_enablehook()
242
        self.assertNotEqual(shell.handler, prev_handler)
243
        try:
244
            no_exception(code)
245
        except:
246
            shell.showtraceback(sys.exc_info())
247
            raise
248
        finally:
249
            self.assertNotEqual(shell.handler, prev_handler)
250
            didyoumean_disablehook()
251
            self.assertEqual(shell.handler, prev_handler)
252
            shell.remove(module)
253
            shell = None
254
255
256
if __name__ == '__main__':
257
    print(sys.version_info)
258
    unittest2.main()
259