Completed
Push — master ( 652866...ac3ef3 )
by De
01:15
created

DummyShell.remove()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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