Completed
Push — master ( d801f1...c47997 )
by De
01:04
created

SyntaxErrorTests.test_octal_literal()   A

Complexity

Conditions 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 1
1
# -*- coding: utf-8
2
"""Unit tests for get_suggestions_for_exception."""
3
from didyoumean_internal import get_suggestions_for_exception, STAND_MODULES
4
import didyoumean_common_tests as common
5
import unittest2
6
import didyoumean_re as re
7
import warnings
8
import sys
9
import math
10
import os
11
import tempfile
12
from shutil import rmtree
13
14
15
this_is_a_global_list = []  # Value does not really matter but the type does
16
17
18
def func_gen(name='some_func', param='', body='pass', args=None):
19
    """Generate code corresponding to a function definition.
20
21
    Generate code for function definition (and eventually a call to it).
22
    Parameters are : name (with default), body (with default),
23
    parameters (with default) and arguments to call the functions with (if not
24
    provided or provided None, function call is not included in generated
25
    code).
26
    """
27
    func = "def {0}({1}):\n\t{2}\n".format(name, param, body)
28
    call = "" if args is None else "{0}({1})\n".format(name, args)
29
    return func + call
30
31
32
def my_generator():
33
    """Generate values for testing purposes.
34
35
    my_generator
36
    This is my generator, baby.
37
    """
38
    for i in range(5):
39
        yield i
40
41
42
class FoobarClass():
43
    """Dummy class for testing purposes."""
44
45
    def __init__(self):
46
        """Constructor."""
47
        self.babar = 2
48
49
    @classmethod
50
    def this_is_cls_mthd(cls):
51
        """Just a class method."""
52
        return 5
53
54
    def nameerror_self(self):
55
        """Should be self.babar."""
56
        return babar
57
58
    def nameerror_self2(self):
59
        """Should be self.this_is_cls_mthd (or FoobarClass)."""
60
        return this_is_cls_mthd
61
62
    @classmethod
63
    def nameerror_cls(cls):
64
        """Should be cls.this_is_cls_mthd (or FoobarClass)."""
65
        return this_is_cls_mthd
66
67
    def some_method(self):
68
        """Method for testing purposes."""
69
        pass
70
71
    def _some_semi_private_method(self):
72
        """Method for testing purposes."""
73
        pass
74
75
    def __some_private_method(self):
76
        """Method for testing purposes."""
77
        pass
78
79
80
# Logic to be able to have different tests on various version of Python
81
FIRST_VERSION = (0, 0)
82
LAST_VERSION = (10, 0)
83
ALL_VERSIONS = (FIRST_VERSION, LAST_VERSION)
84
INTERPRETERS = ['cython', 'pypy']
85
86
87
def from_version(version):
88
    """Create tuple describing a range of versions from a given version."""
89
    return (version, LAST_VERSION)
90
91
92
def up_to_version(version):
93
    """Create tuple describing a range of versions up to a given version."""
94
    return (FIRST_VERSION, version)
95
96
97
def version_in_range(version_range):
98
    """Test if current version is in a range version."""
99
    beg, end = version_range
100
    return beg <= sys.version_info < end
101
102
103
def interpreter_in(interpreters):
104
    """Test if current interpreter is in a list of interpreters."""
105
    is_pypy = hasattr(sys, "pypy_translation_info")
106
    interpreter = 'pypy' if is_pypy else 'cython'
107
    return interpreter in interpreters
108
109
110
def format_str(template, *args):
111
    """Format multiple string by using first arg as a template."""
112
    return [template.format(arg) for arg in args]
113
114
115
def listify(value, default):
116
    """Return list from value, using default value if value is None."""
117
    if value is None:
118
        value = default
119
    if not isinstance(value, list):
120
        value = [value]
121
    return value
122
123
124
def no_exception(code):
125
    """Helper function to run code and check it works."""
126
    exec(code)
127
128
129
def get_exception(code):
130
    """Helper function to run code and get what it throws."""
131
    try:
132
        no_exception(code)
133
    except:
134
        return sys.exc_info()
135
    assert False, "No exception thrown running\n---\n{0}\n---".format(code)
136
137
138
# NameError for NameErrorTests
139
NAMEERROR = (NameError, re.NAMENOTDEFINED_RE)
140
NAMEERRORBEFOREREF = (NameError, re.VARREFBEFOREASSIGN_RE)
141
UNKNOWN_NAMEERROR = (NameError, None)
142
# UnboundLocalError for UnboundLocalErrorTests
143
UNBOUNDLOCAL = (UnboundLocalError, re.VARREFBEFOREASSIGN_RE)
144
UNKNOWN_UNBOUNDLOCAL = (UnboundLocalError, None)
145
# TypeError for TypeErrorTests
146
NBARGERROR = (TypeError, re.NB_ARG_RE)
147
MISSINGPOSERROR = (TypeError, re.MISSING_POS_ARG_RE)
148
UNHASHABLE = (TypeError, re.UNHASHABLE_RE)
149
UNSUBSCRIBTABLE = (TypeError, re.UNSUBSCRIBTABLE_RE)
150
UNEXPECTEDKWARG = (TypeError, re.UNEXPECTED_KEYWORDARG_RE)
151
UNEXPECTEDKWARG2 = (TypeError, re.UNEXPECTED_KEYWORDARG2_RE)
152
UNEXPECTEDKWARG3 = (TypeError, re.UNEXPECTED_KEYWORDARG3_RE)
153
UNSUPPORTEDOPERAND = (TypeError, re.UNSUPPORTED_OP_RE)
154
OBJECTDOESNOTSUPPORT = (TypeError, re.OBJ_DOES_NOT_SUPPORT_RE)
155
CANNOTCONCAT = (TypeError, re.CANNOT_CONCAT_RE)
156
CANTCONVERT = (TypeError, re.CANT_CONVERT_RE)
157
MUSTBETYPENOTTYPE = (TypeError, re.MUST_BE_TYPE1_NOT_TYPE2_RE)
158
NOTCALLABLE = (TypeError, re.NOT_CALLABLE_RE)
159
DESCREXPECT = (TypeError, re.DESCRIPT_REQUIRES_TYPE_RE)
160
ARGNOTITERABLE = (TypeError, re.ARG_NOT_ITERABLE_RE)
161
MUSTCALLWITHINST = (TypeError, re.MUST_BE_CALLED_WITH_INST_RE)
162
OBJECTHASNOFUNC = (TypeError, re.OBJECT_HAS_NO_FUNC_RE)
163
UNKNOWN_TYPEERROR = (TypeError, None)
164
# ImportError for ImportErrorTests
165
NOMODULE = (ImportError, re.NOMODULE_RE)
166
CANNOTIMPORT = (ImportError, re.CANNOTIMPORT_RE)
167
UNKNOWN_IMPORTERROR = (ImportError, None)
168
# KeyError for KeyErrorTests
169
KEYERROR = (KeyError, None)
170
# IndexError for IndexErrorTests
171
OUTOFRANGE = (IndexError, re.INDEXOUTOFRANGE_RE)
172
# ValueError for ValueErrorTests
173
TOOMANYVALUES = (ValueError, re.TOO_MANY_VALUES_UNPACK_RE)
174
NEEDMOREVALUES = (ValueError, re.NEED_MORE_VALUES_RE)
175
EXPECTEDLENGTH = (ValueError, re.EXPECTED_LENGTH_RE)
176
MATHDOMAIN = (ValueError, re.MATH_DOMAIN_ERROR_RE)
177
ZEROLENERROR = (ValueError, re.ZERO_LEN_FIELD_RE)
178
INVALIDLITERAL = (ValueError, re.INVALID_LITERAL_RE)
179
TIMEDATAFORMAT = (ValueError, re.TIME_DATA_DOES_NOT_MATCH_FORMAT_RE)
180
# AttributeError for AttributeErrorTests
181
ATTRIBUTEERROR = (AttributeError, re.ATTRIBUTEERROR_RE)
182
MODATTRIBUTEERROR = (AttributeError, re.MODULEHASNOATTRIBUTE_RE)
183
UNKNOWN_ATTRIBUTEERROR = (AttributeError, None)
184
# SyntaxError for SyntaxErrorTests
185
INVALIDSYNTAX = (SyntaxError, re.INVALID_SYNTAX_RE)
186
INVALIDTOKEN = (SyntaxError, re.INVALID_TOKEN_RE)
187
NOBINDING = (SyntaxError, re.NO_BINDING_NONLOCAL_RE)
188
OUTSIDEFUNC = (SyntaxError, re.OUTSIDE_FUNCTION_RE)
189
MISSINGPARENT = (SyntaxError, re.MISSING_PARENT_RE)
190
INVALIDCOMP = (SyntaxError, re.INVALID_COMP_RE)
191
FUTUREFIRST = (SyntaxError, re.FUTURE_FIRST_RE)
192
FUTFEATNOTDEF = (SyntaxError, re.FUTURE_FEATURE_NOT_DEF_RE)
193
UNQUALIFIED_EXEC = (SyntaxError, re.UNQUALIFIED_EXEC_RE)
194
IMPORTSTAR = (SyntaxError, re.IMPORTSTAR_RE)
195
# MemoryError and OverflowError for MemoryErrorTests
196
MEMORYERROR = (MemoryError, '')
197
OVERFLOWERR = (OverflowError, re.RESULT_TOO_MANY_ITEMS_RE)
198
# IOError
199
NOFILE_IO = (common.NoFileIoError, re.NO_SUCH_FILE_RE)
200
NOFILE_OS = (common.NoFileOsError, re.NO_SUCH_FILE_RE)
201
NOTADIR_IO = (common.NotDirIoError, "^Not a directory$")
202
NOTADIR_OS = (common.NotDirOsError, "^Not a directory$")
203
ISADIR_IO = (common.IsDirIoError, "^Is a directory$")
204
ISADIR_OS = (common.IsDirOsError, "^Is a directory$")
205
DIRNOTEMPTY_OS = (OSError, "^Directory not empty$")
206
207
208
class GetSuggestionsTests(unittest2.TestCase):
209
    """Generic class to test get_suggestions_for_exception.
210
211
    Many tests do not correspond to any handled exceptions but are
212
    kept because it is quite convenient to have a large panel of examples.
213
    Also, some correspond to example where suggestions could be added, those
214
    are flagged with a NICE_TO_HAVE comment.
215
    Finally, whenever it is easily possible, the code with the suggestions
216
    taken into account is usually tested too to ensure that the suggestion does
217
    work.
218
    """
219
220
    def runs(self, code, version_range=None, interpreters=None):
221
        """Helper function to run code.
222
223
        version_range and interpreters can be provided if the test depends on
224
        the used environment.
225
        """
226
        interpreters = listify(interpreters, INTERPRETERS)
227
        if version_range is None:
228
            version_range = ALL_VERSIONS
229
        if version_in_range(version_range) and interpreter_in(interpreters):
230
            no_exception(code)
231
232
    def throws(self, code, error_info,
233
               sugg=None, version_range=None, interpreters=None):
234
        """Run code and check it throws and relevant suggestions are provided.
235
236
        Helper function to run code, check that it throws, what it throws and
237
        that the exception leads to the expected suggestions.
238
        version_range and interpreters can be provided if the test depends on
239
        the used environment.
240
        """
241
        if version_range is None:
242
            version_range = ALL_VERSIONS
243
        interpreters = listify(interpreters, INTERPRETERS)
244
        sugg = listify(sugg, [])
245
        if version_in_range(version_range) and interpreter_in(interpreters):
246
            error_type, error_msg = error_info
247
            type_caught, value, traceback = get_exception(code)
248
            details = "Running following code :\n---\n{0}\n---".format(code)
249
            self.assertTrue(isinstance(value, type_caught))
250
            self.assertTrue(
251
                issubclass(type_caught, error_type),
252
                "{0} ({1}) not a subclass of {2}"
253
                .format(type_caught, value, error_type))
254
            msg = next((a for a in value.args if isinstance(a, str)), '')
255
            if error_msg is not None:
256
                self.assertRegexpMatches(msg, error_msg)
257
            suggestions = sorted(
258
                get_suggestions_for_exception(value, traceback))
259
            self.assertEqual(suggestions, sugg, details)
260
261
262
class NameErrorTests(GetSuggestionsTests):
263
    """Class for tests related to NameError."""
264
265
    def test_local(self):
266
        """Should be 'foo'."""
267
        code = "foo = 0\n{0}"
268
        typo, sugg = "foob", "foo"
269
        bad_code, good_code = format_str(code, typo, sugg)
270
        self.throws(bad_code, NAMEERROR, "'" + sugg + "' (local)")
271
        self.runs(good_code)
272
273
    def test_1_arg(self):
274
        """Should be 'foo'."""
275
        typo, sugg = "foob", "foo"
276
        code = func_gen(param=sugg, body='{0}', args='1')
277
        bad_code, good_code = format_str(code, typo, sugg)
278
        self.throws(bad_code, NAMEERROR, "'" + sugg + "' (local)")
279
        self.runs(good_code)
280
281
    def test_n_args(self):
282
        """Should be 'fool' or 'foot'."""
283
        typo, sugg1, sugg2 = "foob", "foot", "fool"
284
        code = func_gen(param='fool, foot', body='{0}', args='1, 2')
285
        bad, good1, good2 = format_str(code, typo, sugg1, sugg2)
286
        self.throws(bad, NAMEERROR, ["'fool' (local)", "'foot' (local)"])
287
        self.runs(good1)
288
        self.runs(good2)
289
290
    def test_builtin(self):
291
        """Should be 'max'."""
292
        typo, sugg = 'maxi', 'max'
293
        self.throws(typo, NAMEERROR, "'" + sugg + "' (builtin)")
294
        self.runs(sugg)
295
296
    def test_keyword(self):
297
        """Should be 'pass'."""
298
        typo, sugg = 'passs', 'pass'
299
        self.throws(typo, NAMEERROR, "'" + sugg + "' (keyword)")
300
        self.runs(sugg)
301
302
    def test_global(self):
303
        """Should be this_is_a_global_list."""
304
        typo, sugg = 'this_is_a_global_lis', 'this_is_a_global_list'
305
        # just a way to say that this_is_a_global_list is needed in globals
306
        this_is_a_global_list
307
        self.assertFalse(sugg in locals())
308
        self.assertTrue(sugg in globals())
309
        self.throws(typo, NAMEERROR, "'" + sugg + "' (global)")
310
        self.runs(sugg)
311
312
    def test_name(self):
313
        """Should be '__name__'."""
314
        typo, sugg = '__name_', '__name__'
315
        self.throws(typo, NAMEERROR, "'" + sugg + "' (global)")
316
        self.runs(sugg)
317
318
    def test_decorator(self):
319
        """Should be classmethod."""
320
        typo, sugg = "class_method", "classmethod"
321
        code = "@{0}\n" + func_gen()
322
        bad_code, good_code = format_str(code, typo, sugg)
323
        self.throws(bad_code, NAMEERROR, "'" + sugg + "' (builtin)")
324
        self.runs(good_code)
325
326
    def test_import(self):
327
        """Should be math."""
328
        code = 'import math\n{0}'
329
        typo, sugg = 'maths', 'math'
330
        bad_code, good_code = format_str(code, typo, sugg)
331
        self.throws(bad_code, NAMEERROR, "'" + sugg + "' (local)")
332
        self.runs(good_code)
333
334
    def test_import2(self):
335
        """Should be my_imported_math."""
336
        code = 'import math as my_imported_math\n{0}'
337
        typo, sugg = 'my_imported_maths', 'my_imported_math'
338
        bad_code, good_code = format_str(code, typo, sugg)
339
        self.throws(bad_code, NAMEERROR, "'" + sugg + "' (local)")
340
        self.runs(good_code)
341
342
    def test_imported(self):
343
        """Should be math.pi."""
344
        code = 'import math\n{0}'
345
        typo, sugg = 'pi', 'math.pi'
346
        bad_code, good_code = format_str(code, typo, sugg)
347
        self.throws(bad_code, NAMEERROR, "'" + sugg + "'")
348
        self.runs(good_code)
349
350
    def test_imported_twice(self):
351
        """Should be math.pi."""
352
        code = 'import math\nimport math\n{0}'
353
        typo, sugg = 'pi', 'math.pi'
354
        bad_code, good_code = format_str(code, typo, sugg)
355
        self.throws(bad_code, NAMEERROR, "'" + sugg + "'")
356
        self.runs(good_code)
357
358
    def test_not_imported(self):
359
        """Should be random.choice after importing random."""
360
        # This test assumes that `module` is not imported
361
        module, attr = 'random', 'choice'
362
        self.assertFalse(module in locals())
363
        self.assertFalse(module in globals())
364
        self.assertTrue(module in STAND_MODULES)
365
        bad_code = attr
366
        good_code = 'from {0} import {1}\n{2}'.format(module, attr, bad_code)
367
        self.runs(good_code)
368
        self.throws(
369
            bad_code, NAMEERROR,
370
            "'{0}' from {1} (not imported)".format(attr, module))
371
372
    def test_enclosing_scope(self):
373
        """Test that variables from enclosing scope are suggested."""
374
        # NICE_TO_HAVE
375
        typo, sugg = 'foob', 'foo'
376
        code = 'def f():\n\tfoo = 0\n\tdef g():\n\t\t{0}\n\tg()\nf()'
377
        bad_code, good_code = format_str(code, typo, sugg)
378
        self.throws(bad_code, NAMEERROR)
379
        self.runs(good_code)
380
381
    def test_no_sugg(self):
382
        """No suggestion."""
383
        self.throws('a = ldkjhfnvdlkjhvgfdhgf', NAMEERROR)
384
385
    def test_free_var_before_assignment(self):
386
        """No suggestion but different error message."""
387
        code = 'def f():\n\tdef g():\n\t\treturn free_var' \
388
               '\n\tg()\n\tfree_var = 0\nf()'
389
        self.throws(code, NAMEERRORBEFOREREF)
390
391
    # For added/removed names, following functions with one name
392
    # per functions were added in the early stages of the project.
393
    # In the future, I'd like to have them replaced by something
394
    # a bit more concise using relevant data structure. In the
395
    # meantime, I am keeping both versions because safer is better.
396
    def test_removed_cmp(self):
397
        """Builtin cmp is removed."""
398
        # NICE_TO_HAVE
399
        code = 'cmp(1, 2)'
400
        version = (3, 0, 1)
401
        self.runs(code, up_to_version(version))
402
        self.throws(code, NAMEERROR, [], from_version(version))
403
404
    def test_removed_reduce(self):
405
        """Builtin reduce is removed - moved to functools."""
406
        code = 'reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])'
407
        version = (3, 0)
408
        self.runs(code, up_to_version(version))
409
        self.runs('from functools import reduce\n' + code,
410
                  from_version(version))
411
        self.throws(
412
            code,
413
            NAMEERROR,
414
            "'reduce' from functools (not imported)",
415
            from_version(version))
416
417
    def test_removed_apply(self):
418
        """Builtin apply is removed."""
419
        # NICE_TO_HAVE
420
        code = 'apply(sum, [[1, 2, 3]])'
421
        version = (3, 0)
422
        self.runs(code, up_to_version(version))
423
        self.throws(code, NAMEERROR, [], from_version(version))
424
425
    def test_removed_reload(self):
426
        """Builtin reload is removed.
427
428
        Moved to importlib.reload or imp.reload depending on version.
429
        """
430
        # NICE_TO_HAVE
431
        code = 'reload(math)'
432
        version = (3, 0)
433
        self.runs(code, up_to_version(version))
434
        self.throws(code, NAMEERROR, [], from_version(version))
435
436
    def test_removed_intern(self):
437
        """Builtin intern is removed - moved to sys."""
438
        code = 'intern("toto")'
439
        new_code = 'sys.intern("toto")'
440
        version = (3, 0)
441
        self.runs(code, up_to_version(version))
442
        self.throws(
443
            code, NAMEERROR,
444
            ["'iter' (builtin)", "'sys.intern'"],
445
            from_version(version))
446
        self.runs(new_code, from_version(version))
447
448
    def test_removed_execfile(self):
449
        """Builtin execfile is removed - use exec() and compile()."""
450
        # NICE_TO_HAVE
451
        code = 'execfile("some_filename")'
452
        version = (3, 0)
453
        # self.runs(code, up_to_version(version))
454
        self.throws(code, NAMEERROR, [], from_version(version))
455
456
    def test_removed_raw_input(self):
457
        """Builtin raw_input is removed - use input() instead."""
458
        code = 'i = raw_input("Prompt:")'
459
        version = (3, 0)
460
        # self.runs(code, up_to_version(version))
461
        self.throws(
462
            code, NAMEERROR, "'input' (builtin)", from_version(version))
463
464
    def test_removed_buffer(self):
465
        """Builtin buffer is removed - use memoryview instead."""
466
        # NICE_TO_HAVE
467
        code = 'buffer("abc")'
468
        version = (3, 0)
469
        self.runs(code, up_to_version(version))
470
        self.throws(code, NAMEERROR, [], from_version(version))
471
472
    def test_added_2_7(self):
473
        """Test for names added in 2.7."""
474
        version = (2, 7)
475
        for name in ['memoryview']:
476
            self.runs(name, from_version(version))
477
            self.throws(name, NAMEERROR, [], up_to_version(version))
478
479
    def test_removed_3_0(self):
480
        """Test for names removed in 3.0."""
481
        version = (3, 0)
482
        for name, suggs in {
483
                'StandardError': [],  # Exception
484
                'apply': [],
485
                'basestring': [],
486
                'buffer': [],
487
                'cmp': [],
488
                'coerce': [],
489
                'execfile': [],
490
                'file': ["'filter' (builtin)"],
491
                'intern': ["'iter' (builtin)", "'sys.intern'"],
492
                'long': [],
493
                'raw_input': ["'input' (builtin)"],
494
                'reduce': ["'reduce' from functools (not imported)"],
495
                'reload': [],
496
                'unichr': [],
497
                'unicode': ["'code' (local)"],
498
                'xrange': ["'range' (builtin)"],
499
                }.items():
500
            self.throws(name, NAMEERROR, suggs, from_version(version))
501
            self.runs(name, up_to_version(version))
502
503
    def test_added_3_0(self):
504
        """Test for names added in 3.0."""
505
        version = (3, 0)
506
        for name, suggs in {
507
                'ascii': [],
508
                'ResourceWarning': ["'FutureWarning' (builtin)"],
509
                '__build_class__': [],
510
                }.items():
511
            self.runs(name, from_version(version))
512
            self.throws(name, NAMEERROR, suggs, up_to_version(version))
513
514
    def test_added_3_3(self):
515
        """Test for names added in 3.3."""
516
        version = (3, 3)
517
        for name, suggs in {
518
                'BrokenPipeError': [],
519
                'ChildProcessError': [],
520
                'ConnectionAbortedError': [],
521
                'ConnectionError': ["'IndentationError' (builtin)"],
522
                'ConnectionRefusedError': [],
523
                'ConnectionResetError': [],
524
                'FileExistsError': [],
525
                'FileNotFoundError': [],
526
                'InterruptedError': [],
527
                'IsADirectoryError': [],
528
                'NotADirectoryError': [],
529
                'PermissionError': ["'ZeroDivisionError' (builtin)"],
530
                'ProcessLookupError': ["'LookupError' (builtin)"],
531
                'TimeoutError': [],
532
                '__loader__': [],
533
                }.items():
534
            self.runs(name, from_version(version))
535
            self.throws(name, NAMEERROR, suggs, up_to_version(version))
536
537
    def test_added_3_4(self):
538
        """Test for names added in 3.4."""
539
        version = (3, 4)
540
        for name, suggs in {
541
                '__spec__': [],
542
                }.items():
543
            self.runs(name, from_version(version))
544
            self.throws(name, NAMEERROR, suggs, up_to_version(version))
545
546
    def test_added_3_5(self):
547
        """Test for names added in 3.5."""
548
        version = (3, 5)
549
        for name, suggs in {
550
                'StopAsyncIteration': ["'StopIteration' (builtin)"],
551
                }.items():
552
            self.runs(name, from_version(version))
553
            self.throws(name, NAMEERROR, suggs, up_to_version(version))
554
555
    def test_import_sugg(self):
556
        """Should import module first."""
557
        module = 'collections'
558
        sugg = 'import {0}'.format(module)
559
        typo, good_code = module, sugg + '\n' + module
560
        self.assertFalse(module in locals())
561
        self.assertFalse(module in globals())
562
        self.assertTrue(module in STAND_MODULES)
563
        suggestions = (
564
            # module.module is suggested on Python 3.3 :-/
565
            ["'{0}' from {1} (not imported)".format(module, module)]
566
            if version_in_range(((3, 3), (3, 4))) else []) + \
567
            ['to {0} first'.format(sugg)]
568
        self.throws(typo, NAMEERROR, suggestions)
569
        self.runs(good_code)
570
571
    def test_attribute_hidden(self):
572
        """Should be math.pi but module math is hidden."""
573
        math  # just a way to say that math module is needed in globals
574
        self.assertFalse('math' in locals())
575
        self.assertTrue('math' in globals())
576
        code = 'math = ""\npi'
577
        self.throws(code, NAMEERROR, "'math.pi' (global hidden by local)")
578
579
    def test_self(self):
580
        """"Should be self.babar."""
581
        self.throws(
582
            'FoobarClass().nameerror_self()',
583
            NAMEERROR, "'self.babar'")
584
585
    def test_self2(self):
586
        """Should be self.this_is_cls_mthd."""
587
        self.throws(
588
            'FoobarClass().nameerror_self2()', NAMEERROR,
589
            ["'FoobarClass.this_is_cls_mthd'", "'self.this_is_cls_mthd'"])
590
591
    def test_cls(self):
592
        """Should be cls.this_is_cls_mthd."""
593
        self.throws(
594
            'FoobarClass().nameerror_cls()', NAMEERROR,
595
            ["'FoobarClass.this_is_cls_mthd'", "'cls.this_is_cls_mthd'"])
596
597
    def test_complex_numbers(self):
598
        """Should be 1j."""
599
        code = 'assert {0} ** 2 == -1'
600
        sugg = '1j'
601
        good_code, bad_code_i, bad_code_j = format_str(code, sugg, 'i', 'j')
602
        suggestion = "'" + sugg + "' (imaginary unit)"
603
        self.throws(bad_code_i, NAMEERROR, suggestion)
604
        self.throws(bad_code_j, NAMEERROR, suggestion)
605
        self.runs(good_code)
606
607
    def test_shell_commands(self):
608
        """Trying shell commands."""
609
        cmd, sugg = 'ls', 'os.listdir(os.getcwd())'
610
        self.throws(cmd, NAMEERROR, "'" + sugg + "'")
611
        self.runs(sugg)
612
        cmd, sugg = 'pwd', 'os.getcwd()'
613
        self.throws(cmd, NAMEERROR, "'" + sugg + "'")
614
        self.runs(sugg)
615
        cmd, sugg = 'cd', 'os.chdir(path)'
616
        self.throws(cmd, NAMEERROR, "'" + sugg + "'")
617
        self.runs(sugg.replace('path', 'os.getcwd()'))
618
        cmd = 'rm'
619
        sugg = "'os.remove(filename)', 'shutil.rmtree(dir)' for recursive"
620
        self.throws(cmd, NAMEERROR, sugg)
621
622
    def test_unmatched_msg(self):
623
        """Test that arbitrary strings are supported."""
624
        self.throws(
625
            'raise NameError("unmatched NAMEERROR")',
626
            UNKNOWN_NAMEERROR)
627
628
629
class UnboundLocalErrorTests(GetSuggestionsTests):
630
    """Class for tests related to UnboundLocalError."""
631
632
    def test_unbound_typo(self):
633
        """Should be foo."""
634
        code = 'def func():\n\tfoo = 1\n\t{0} +=1\nfunc()'
635
        typo, sugg = "foob", "foo"
636
        bad_code, good_code = format_str(code, typo, sugg)
637
        self.throws(bad_code, UNBOUNDLOCAL, "'" + sugg + "' (local)")
638
        self.runs(good_code)
639
640
    def test_unbound_global(self):
641
        """Should be global nb."""
642
        # NICE_TO_HAVE
643
        code = 'nb = 0\ndef func():\n\t{0}nb +=1\nfunc()'
644
        sugg = 'global nb'
645
        bad_code, good_code = format_str(code, "", sugg + "\n\t")
646
        self.throws(bad_code, UNBOUNDLOCAL)
647
        self.runs(good_code)  # this is to be run afterward :-/
648
649
    def test_unmatched_msg(self):
650
        """Test that arbitrary strings are supported."""
651
        self.throws(
652
            'raise UnboundLocalError("unmatched UNBOUNDLOCAL")',
653
            UNKNOWN_UNBOUNDLOCAL)
654
655
656
class AttributeErrorTests(GetSuggestionsTests):
657
    """Class for tests related to AttributeError."""
658
659
    def test_nonetype(self):
660
        """In-place methods like sort returns None.
661
662
        Might also happen if the functions misses a return.
663
        """
664
        # NICE_TO_HAVE
665
        code = '[].sort().append(4)'
666
        self.throws(code, ATTRIBUTEERROR)
667
668
    def test_method(self):
669
        """Should be 'append'."""
670
        code = '[0].{0}(1)'
671
        typo, sugg = 'appendh', 'append'
672
        bad_code, good_code = format_str(code, typo, sugg)
673
        self.throws(bad_code, ATTRIBUTEERROR, "'" + sugg + "'")
674
        self.runs(good_code)
675
676
    def test_builtin(self):
677
        """Should be 'max(lst)'."""
678
        bad_code, good_code = '[0].max()', 'max([0])'
679
        self.throws(bad_code, ATTRIBUTEERROR, "'max(list)'")
680
        self.runs(good_code)
681
682
    def test_builtin2(self):
683
        """Should be 'next(gen)'."""
684
        code = 'my_generator().next()'
685
        new_code = 'next(my_generator())'
686
        version = (3, 0)
687
        self.runs(code, up_to_version(version))
688
        self.throws(
689
            code, ATTRIBUTEERROR,
690
            "'next(generator)'",
691
            from_version(version))
692
        self.runs(new_code)
693
694
    def test_wrongmethod(self):
695
        """Should be 'lst.append(1)'."""
696
        code = '[0].{0}(1)'
697
        typo, sugg = 'add', 'append'
698
        bad_code, good_code = format_str(code, typo, sugg)
699
        self.throws(bad_code, ATTRIBUTEERROR, "'" + sugg + "'")
700
        self.runs(good_code)
701
702
    def test_wrongmethod2(self):
703
        """Should be 'lst.extend([4, 5, 6])'."""
704
        code = '[0].{0}([4, 5, 6])'
705
        typo, sugg = 'update', 'extend'
706
        bad_code, good_code = format_str(code, typo, sugg)
707
        self.throws(bad_code, ATTRIBUTEERROR, "'" + sugg + "'")
708
        self.runs(good_code)
709
710
    def test_hidden(self):
711
        """Accessing wrong string object."""
712
        # NICE_TO_HAVE
713
        code = 'import string\nstring = "a"\nascii = string.ascii_letters'
714
        self.throws(code, ATTRIBUTEERROR)
715
716
    def test_no_sugg(self):
717
        """No suggestion."""
718
        self.throws('[1, 2, 3].ldkjhfnvdlkjhvgfdhgf', ATTRIBUTEERROR)
719
720
    def test_from_module(self):
721
        """Should be math.pi."""
722
        code = 'import math\nmath.{0}'
723
        typo, sugg = 'pie', 'pi'
724
        version = (3, 5)
725
        bad_code, good_code = format_str(code, typo, sugg)
726
        self.throws(bad_code, ATTRIBUTEERROR, "'" + sugg + "'",
727
                    up_to_version(version))
728
        self.throws(bad_code, MODATTRIBUTEERROR, "'" + sugg + "'",
729
                    from_version(version))
730
        self.runs(good_code)
731
732
    def test_from_module2(self):
733
        """Should be math.pi."""
734
        code = 'import math\nm = math\nm.{0}'
735
        typo, sugg = 'pie', 'pi'
736
        version = (3, 5)
737
        bad_code, good_code = format_str(code, typo, sugg)
738
        self.throws(bad_code, ATTRIBUTEERROR, "'" + sugg + "'",
739
                    up_to_version(version))
740
        self.throws(bad_code, MODATTRIBUTEERROR, "'" + sugg + "'",
741
                    from_version(version))
742
        self.runs(good_code)
743
744
    def test_from_class(self):
745
        """Should be 'this_is_cls_mthd'."""
746
        code = 'FoobarClass().{0}()'
747
        typo, sugg = 'this_is_cls_mth', 'this_is_cls_mthd'
748
        bad_code, good_code = format_str(code, typo, sugg)
749
        self.throws(bad_code, ATTRIBUTEERROR, "'" + sugg + "'")
750
        self.runs(good_code)
751
752
    def test_from_class2(self):
753
        """Should be 'this_is_cls_mthd'."""
754
        code = 'FoobarClass.{0}()'
755
        typo, sugg = 'this_is_cls_mth', 'this_is_cls_mthd'
756
        bad_code, good_code = format_str(code, typo, sugg)
757
        self.throws(bad_code, ATTRIBUTEERROR, "'" + sugg + "'")
758
        self.runs(good_code)
759
760
    def test_private_attr(self):
761
        """Test that 'private' members are suggested with a warning message.
762
763
        Sometimes 'private' members are suggested but it's not ideal, a
764
        warning must be added to the suggestion.
765
        """
766
        code = 'FoobarClass().{0}'
767
        method = '__some_private_method'
768
        method2 = '_some_semi_private_method'
769
        typo, sugg, sugg2 = method, '_FoobarClass' + method, method2
770
        bad_code, bad_sugg, good_sugg = format_str(code, typo, sugg, sugg2)
771
        self.throws(
772
            bad_code,
773
            ATTRIBUTEERROR,
774
            ["'{0}' (but it is supposed to be private)".format(sugg),
775
             "'{0}'".format(sugg2)])
776
        self.runs(bad_sugg)
777
        self.runs(good_sugg)
778
779
    def test_removed_has_key(self):
780
        """Method has_key is removed from dict."""
781
        code = 'dict().has_key(1)'
782
        new_code = '1 in dict()'
783
        version = (3, 0)
784
        self.runs(code, up_to_version(version))
785
        self.throws(
786
            code,
787
            ATTRIBUTEERROR,
788
            "'key in dict'", from_version(version))
789
        self.runs(new_code)
790
791
    def test_removed_xreadlines(self):
792
        """Method xreadlines is removed."""
793
        # NICE_TO_HAVE
794
        code = "import os\nwith open(os.path.realpath(__file__)) as f:" \
795
            "\n\tf.{0}"
796
        old, sugg1, sugg2 = 'xreadlines', 'readline', 'readlines'
797
        old_code, new_code1, new_code2 = format_str(code, old, sugg1, sugg2)
798
        version = (3, 0)
799
        self.runs(old_code, up_to_version(version))
800
        self.throws(
801
            old_code,
802
            ATTRIBUTEERROR,
803
            ["'" + sugg1 + "'", "'" + sugg2 + "'", "'writelines'"],
804
            from_version(version))
805
        self.runs(new_code1)
806
        self.runs(new_code2)
807
808
    def test_removed_function_attributes(self):
809
        """Some functions attributes are removed."""
810
        # NICE_TO_HAVE
811
        version = (3, 0)
812
        code = func_gen() + 'some_func.{0}'
813
        attributes = [('func_name', '__name__', []),
814
                      ('func_doc', '__doc__', []),
815
                      ('func_defaults', '__defaults__', ["'__defaults__'"]),
816
                      ('func_dict', '__dict__', []),
817
                      ('func_closure', '__closure__', []),
818
                      ('func_globals', '__globals__', []),
819
                      ('func_code', '__code__', [])]
820
        for (old_att, new_att, sugg) in attributes:
821
            old_code, new_code = format_str(code, old_att, new_att)
822
            self.runs(old_code, up_to_version(version))
823
            self.throws(old_code, ATTRIBUTEERROR, sugg, from_version(version))
824
            self.runs(new_code)
825
826
    def test_removed_method_attributes(self):
827
        """Some methods attributes are removed."""
828
        # NICE_TO_HAVE
829
        version = (3, 0)
830
        code = 'FoobarClass().some_method.{0}'
831
        attributes = [('im_func', '__func__', []),
832
                      ('im_self', '__self__', []),
833
                      ('im_class', '__self__.__class__', ["'__class__'"])]
834
        for (old_att, new_att, sugg) in attributes:
835
            old_code, new_code = format_str(code, old_att, new_att)
836
            self.runs(old_code, up_to_version(version))
837
            self.throws(old_code, ATTRIBUTEERROR, sugg, from_version(version))
838
            self.runs(new_code)
839
840
    def test_join(self):
841
        """Test what happens when join is used incorrectly.
842
843
        This can be frustrating to call join on an iterable instead of a
844
        string, a suggestion could be nice.
845
        """
846
        # NICE_TO_HAVE
847
        code = "['a', 'b'].join('-')"
848
        self.throws(code, ATTRIBUTEERROR)
849
850
    def test_set_dict_comprehension(self):
851
        """{} creates a dict and not an empty set leading to errors."""
852
        # NICE_TO_HAVE
853
        version = (2, 7)
854
        for method in set(dir(set)) - set(dir(dict)):
855
            if not method.startswith('__'):  # boring suggestions
856
                code = "a = {0}\na." + method
857
                typo, dict1, dict2, sugg, set1 = format_str(
858
                    code, "{}", "dict()", "{0: 0}", "set()", "{0}")
859
                self.throws(typo, ATTRIBUTEERROR)
860
                self.throws(dict1, ATTRIBUTEERROR)
861
                self.throws(dict2, ATTRIBUTEERROR)
862
                self.runs(sugg)
863
                self.throws(set1, INVALIDSYNTAX, [], up_to_version(version))
864
                self.runs(set1, from_version(version))
865
866
    def test_unmatched_msg(self):
867
        """Test that arbitrary strings are supported."""
868
        self.throws(
869
            'raise AttributeError("unmatched ATTRIBUTEERROR")',
870
            UNKNOWN_ATTRIBUTEERROR)
871
872
    # TODO: Add sugg for situation where self/cls is the missing parameter
873
874
875
class TypeErrorTests(GetSuggestionsTests):
876
    """Class for tests related to TypeError."""
877
878
    def test_unhashable(self):
879
        """Test that other errors do not crash."""
880
        self.throws('dict()[list()] = 1', UNHASHABLE)
881
882
    def test_not_sub(self):
883
        """Should be function call, not [] operator."""
884
        typo, sugg = '[2]', '(2)'
885
        code = func_gen(param='a') + 'some_func{0}'
886
        bad_code, good_code = format_str(code, typo, sugg)
887
        self.throws(bad_code, UNSUBSCRIBTABLE, "'function(value)'")
888
        self.runs(good_code)
889
890
    def test_method_called_on_class(self):
891
        """Test where a method is called on a class and not an instance.
892
893
        Forgetting parenthesis makes the difference between using an
894
        instance and using a type.
895
        """
896
        # NICE_TO_HAVE
897
        wrong_type = (DESCREXPECT, MUSTCALLWITHINST, NBARGERROR)
898
        not_iterable = (ARGNOTITERABLE, ARGNOTITERABLE, ARGNOTITERABLE)
899
        version = (3, 0)
900
        for code, (err_cy, err_pyp, err_pyp3) in [
901
                ('set{0}.add(0)', wrong_type),
902
                ('list{0}.append(0)', wrong_type),
903
                ('0 in list{0}', not_iterable)]:
904
            bad_code, good_code = format_str(code, '', '()')
905
            self.runs(good_code)
906
            self.throws(bad_code, err_cy, [], ALL_VERSIONS, 'cython')
907
            self.throws(bad_code, err_pyp, [], up_to_version(version), 'pypy')
908
            self.throws(bad_code, err_pyp3, [], from_version(version), 'pypy')
909
910
    def test_set_operations(self):
911
        """+, +=, etc doesn't work on sets. A suggestion would be nice."""
912
        # NICE_TO_HAVE
913
        typo1 = 'set() + set()'
914
        typo2 = 's = set()\ns += set()'
915
        code1 = 'set() | set()'
916
        code2 = 'set().union(set())'
917
        code3 = 'set().update(set())'
918
        self.throws(typo1, UNSUPPORTEDOPERAND)
919
        self.throws(typo2, UNSUPPORTEDOPERAND)
920
        self.runs(code1)
921
        self.runs(code2)
922
        self.runs(code3)
923
924
    def test_dict_operations(self):
925
        """+, +=, etc doesn't work on dicts. A suggestion would be nice."""
926
        # NICE_TO_HAVE
927
        typo1 = 'dict() + dict()'
928
        typo2 = 'd = dict()\nd += dict()'
929
        typo3 = 'dict() & dict()'
930
        self.throws(typo1, UNSUPPORTEDOPERAND)
931
        self.throws(typo2, UNSUPPORTEDOPERAND)
932
        self.throws(typo3, UNSUPPORTEDOPERAND)
933
        code1 = 'dict().update(dict())'
934
        self.runs(code1)
935
936
    def test_len_on_iterable(self):
937
        """len() can't be called on iterable (weird but understandable)."""
938
        code = 'len(my_generator())'
939
        sugg = 'len(list(my_generator()))'
940
        self.throws(code, OBJECTHASNOFUNC)
941
        self.runs(sugg)
942
943
    def test_nb_args(self):
944
        """Should have 1 arg."""
945
        typo, sugg = '1, 2', '1'
946
        code = func_gen(param='a', args='{0}')
947
        bad_code, good_code = format_str(code, typo, sugg)
948
        self.throws(bad_code, NBARGERROR)
949
        self.runs(good_code)
950
951
    def test_nb_args1(self):
952
        """Should have 0 args."""
953
        typo, sugg = '1', ''
954
        code = func_gen(param='', args='{0}')
955
        bad_code, good_code = format_str(code, typo, sugg)
956
        self.throws(bad_code, NBARGERROR)
957
        self.runs(good_code)
958
959
    def test_nb_args2(self):
960
        """Should have 1 arg."""
961
        typo, sugg = '', '1'
962
        version = (3, 3)
963
        code = func_gen(param='a', args='{0}')
964
        bad_code, good_code = format_str(code, typo, sugg)
965
        self.throws(bad_code, NBARGERROR, [], up_to_version(version))
966
        self.throws(bad_code, MISSINGPOSERROR, [], from_version(version))
967
        self.runs(good_code)
968
969
    def test_nb_args3(self):
970
        """Should have 3 args."""
971
        typo, sugg = '1', '1, 2, 3'
972
        version = (3, 3)
973
        code = func_gen(param='so, much, args', args='{0}')
974
        bad_code, good_code = format_str(code, typo, sugg)
975
        self.throws(bad_code, NBARGERROR, [], up_to_version(version))
976
        self.throws(bad_code, MISSINGPOSERROR, [], from_version(version))
977
        self.runs(good_code)
978
979
    def test_nb_args4(self):
980
        """Should have 3 args."""
981
        typo, sugg = '', '1, 2, 3'
982
        version = (3, 3)
983
        code = func_gen(param='so, much, args', args='{0}')
984
        bad_code, good_code = format_str(code, typo, sugg)
985
        self.throws(bad_code, NBARGERROR, [], up_to_version(version))
986
        self.throws(bad_code, MISSINGPOSERROR, [], from_version(version))
987
        self.runs(good_code)
988
989
    def test_nb_args5(self):
990
        """Should have 3 args."""
991
        typo, sugg = '1, 2', '1, 2, 3'
992
        version = (3, 3)
993
        code = func_gen(param='so, much, args', args='{0}')
994
        bad_code, good_code = format_str(code, typo, sugg)
995
        self.throws(bad_code, NBARGERROR, [], up_to_version(version))
996
        self.throws(bad_code, MISSINGPOSERROR, [], from_version(version))
997
        self.runs(good_code)
998
999
    def test_keyword_args(self):
1000
        """Should be param 'babar' not 'a' but it's hard to guess."""
1001
        typo, sugg = 'a', 'babar'
1002
        code = func_gen(param=sugg, args='{0}=1')
1003
        bad_code, good_code = format_str(code, typo, sugg)
1004
        self.throws(bad_code, UNEXPECTEDKWARG)
1005
        self.runs(good_code)
1006
1007
    def test_keyword_args2(self):
1008
        """Should be param 'abcdef' not 'abcdf'."""
1009
        typo, sugg = 'abcdf', 'abcdef'
1010
        code = func_gen(param=sugg, args='{0}=1')
1011
        bad_code, good_code = format_str(code, typo, sugg)
1012
        self.throws(bad_code, UNEXPECTEDKWARG, "'" + sugg + "'")
1013
        self.runs(good_code)
1014
1015
    def test_keyword_builtin(self):
1016
        """A few builtins (like int()) have a different error message."""
1017
        # NICE_TO_HAVE
1018
        # 'max', 'input', 'len', 'abs', 'all', etc have a specific error
1019
        # message and are not relevant here
1020
        for builtin in ['int', 'float', 'bool', 'complex']:
1021
            code = builtin + '(this_doesnt_exist=2)'
1022
            self.throws(code, UNEXPECTEDKWARG2, [], ALL_VERSIONS, 'cython')
1023
            self.throws(code, UNEXPECTEDKWARG, [], ALL_VERSIONS, 'pypy')
1024
1025
    def test_keyword_builtin_print(self):
1026
        """Builtin "print" has a different error message."""
1027
        # It would be NICE_TO_HAVE suggestions on keyword arguments
1028
        v3 = (3, 0)
1029
        code = "c = 'string'\nb = print(c, end_='toto')"
1030
        self.throws(code, INVALIDSYNTAX, [], up_to_version(v3))
1031
        self.throws(code, UNEXPECTEDKWARG2, [], from_version(v3), 'cython')
1032
        self.throws(code, UNEXPECTEDKWARG3, [], from_version(v3), 'pypy')
1033
1034
    def test_no_implicit_str_conv(self):
1035
        """Trying to concatenate a non-string value to a string."""
1036
        # NICE_TO_HAVE
1037
        code = '{0} + " things"'
1038
        typo, sugg = '12', 'str(12)'
1039
        bad_code, good_code = format_str(code, typo, sugg)
1040
        self.throws(bad_code, UNSUPPORTEDOPERAND)
1041
        self.runs(good_code)
1042
1043
    def test_no_implicit_str_conv2(self):
1044
        """Trying to concatenate a non-string value to a string."""
1045
        # NICE_TO_HAVE
1046
        code = '"things " + {0}'
1047
        typo, sugg = '12', 'str(12)'
1048
        bad_code, good_code = format_str(code, typo, sugg)
1049
        version = (3, 0)
1050
        version2 = (3, 6)
1051
        self.throws(
1052
            bad_code, CANNOTCONCAT, [], up_to_version(version), 'cython')
1053
        self.throws(
1054
            bad_code, CANTCONVERT, [], (version, version2), 'cython')
1055
        self.throws(
1056
            bad_code, MUSTBETYPENOTTYPE, [], from_version(version2), 'cython')
1057
        self.throws(
1058
            bad_code, UNSUPPORTEDOPERAND, [], ALL_VERSIONS, 'pypy')
1059
        self.runs(good_code)
1060
1061
    def test_assignment_to_range(self):
1062
        """Trying to assign to range works on list, not on range."""
1063
        # NICE_TO_HAVE
1064
        code = '{0}[2] = 1'
1065
        typo, sugg = 'range(4)', 'list(range(4))'
1066
        version = (3, 0)
1067
        bad_code, good_code = format_str(code, typo, sugg)
1068
        self.runs(bad_code, up_to_version(version))
1069
        self.throws(bad_code, OBJECTDOESNOTSUPPORT, [], from_version(version))
1070
        self.runs(good_code)
1071
1072
    def test_not_callable(self):
1073
        """Sometimes, one uses parenthesis instead of brackets."""
1074
        typo, getitem = '(0)', '[0]'
1075
        for ex, sugg in {
1076
            '[0]': "'list[value]'",
1077
            '{0: 0}': "'dict[value]'",
1078
            '"a"': "'str[value]'",
1079
        }.items():
1080
            self.throws(ex + typo, NOTCALLABLE, sugg)
1081
            self.runs(ex + getitem)
1082
        for ex in ['1', 'set()']:
1083
            self.throws(ex + typo, NOTCALLABLE)
1084
1085
    def test_unmatched_msg(self):
1086
        """Test that arbitrary strings are supported."""
1087
        self.throws(
1088
            'raise TypeError("unmatched TYPEERROR")',
1089
            UNKNOWN_TYPEERROR)
1090
1091
1092
class ImportErrorTests(GetSuggestionsTests):
1093
    """Class for tests related to ImportError."""
1094
1095
    def test_no_module_no_sugg(self):
1096
        """No suggestion."""
1097
        self.throws('import fqslkdfjslkqdjfqsd', NOMODULE)
1098
1099
    def test_no_module(self):
1100
        """Should be 'math'."""
1101
        code = 'import {0}'
1102
        typo, sugg = 'maths', 'math'
1103
        self.assertTrue(sugg in STAND_MODULES)
1104
        bad_code, good_code = format_str(code, typo, sugg)
1105
        self.throws(bad_code, NOMODULE, "'" + sugg + "'")
1106
        self.runs(good_code)
1107
1108
    def test_no_module2(self):
1109
        """Should be 'math'."""
1110
        code = 'from {0} import pi'
1111
        typo, sugg = 'maths', 'math'
1112
        self.assertTrue(sugg in STAND_MODULES)
1113
        bad_code, good_code = format_str(code, typo, sugg)
1114
        self.throws(bad_code, NOMODULE, "'" + sugg + "'")
1115
        self.runs(good_code)
1116
1117
    def test_no_module3(self):
1118
        """Should be 'math'."""
1119
        code = 'import {0} as my_imported_math'
1120
        typo, sugg = 'maths', 'math'
1121
        self.assertTrue(sugg in STAND_MODULES)
1122
        bad_code, good_code = format_str(code, typo, sugg)
1123
        self.throws(bad_code, NOMODULE, "'" + sugg + "'")
1124
        self.runs(good_code)
1125
1126
    def test_no_module4(self):
1127
        """Should be 'math'."""
1128
        code = 'from {0} import pi as three_something'
1129
        typo, sugg = 'maths', 'math'
1130
        self.assertTrue(sugg in STAND_MODULES)
1131
        bad_code, good_code = format_str(code, typo, sugg)
1132
        self.throws(bad_code, NOMODULE, "'" + sugg + "'")
1133
        self.runs(good_code)
1134
1135
    def test_no_module5(self):
1136
        """Should be 'math'."""
1137
        code = '__import__("{0}")'
1138
        typo, sugg = 'maths', 'math'
1139
        self.assertTrue(sugg in STAND_MODULES)
1140
        bad_code, good_code = format_str(code, typo, sugg)
1141
        self.throws(bad_code, NOMODULE, "'" + sugg + "'")
1142
        self.runs(good_code)
1143
1144
    def test_import_future_nomodule(self):
1145
        """Should be '__future__'."""
1146
        code = 'import {0}'
1147
        typo, sugg = '__future_', '__future__'
1148
        self.assertTrue(sugg in STAND_MODULES)
1149
        bad_code, good_code = format_str(code, typo, sugg)
1150
        self.throws(bad_code, NOMODULE, "'" + sugg + "'")
1151
        self.runs(good_code)
1152
1153
    def test_no_name_no_sugg(self):
1154
        """No suggestion."""
1155
        self.throws('from math import fsfsdfdjlkf', CANNOTIMPORT)
1156
1157
    def test_wrong_import(self):
1158
        """Should be 'math'."""
1159
        code = 'from {0} import pi'
1160
        typo, sugg = 'itertools', 'math'
1161
        self.assertTrue(sugg in STAND_MODULES)
1162
        bad_code, good_code = format_str(code, typo, sugg)
1163
        self.throws(bad_code, CANNOTIMPORT, "'" + good_code + "'")
1164
        self.runs(good_code)
1165
1166
    def test_typo_in_method(self):
1167
        """Should be 'pi'."""
1168
        code = 'from math import {0}'
1169
        typo, sugg = 'pie', 'pi'
1170
        bad_code, good_code = format_str(code, typo, sugg)
1171
        self.throws(bad_code, CANNOTIMPORT, "'" + sugg + "'")
1172
        self.runs(good_code)
1173
1174
    def test_typo_in_method2(self):
1175
        """Should be 'pi'."""
1176
        code = 'from math import e, {0}, log'
1177
        typo, sugg = 'pie', 'pi'
1178
        bad_code, good_code = format_str(code, typo, sugg)
1179
        self.throws(bad_code, CANNOTIMPORT, "'" + sugg + "'")
1180
        self.runs(good_code)
1181
1182
    def test_typo_in_method3(self):
1183
        """Should be 'pi'."""
1184
        code = 'from math import {0} as three_something'
1185
        typo, sugg = 'pie', 'pi'
1186
        bad_code, good_code = format_str(code, typo, sugg)
1187
        self.throws(bad_code, CANNOTIMPORT, "'" + sugg + "'")
1188
        self.runs(good_code)
1189
1190
    def test_unmatched_msg(self):
1191
        """Test that arbitrary strings are supported."""
1192
        self.throws(
1193
            'raise ImportError("unmatched IMPORTERROR")',
1194
            UNKNOWN_IMPORTERROR)
1195
1196
    def test_module_removed(self):
1197
        """Sometimes, modules are deleted/moved/renamed."""
1198
        # NICE_TO_HAVE
1199
        version1 = (2, 7)  # result for 2.6 seems to vary
1200
        version2 = (3, 0)
1201
        code = 'import {0}'
1202
        lower, upper = format_str(code, 'tkinter', 'Tkinter')
1203
        self.throws(lower, NOMODULE, [], (version1, version2))
1204
        self.throws(upper, NOMODULE, [], from_version(version2))
1205
1206
1207
class LookupErrorTests(GetSuggestionsTests):
1208
    """Class for tests related to LookupError."""
1209
1210
1211
class KeyErrorTests(LookupErrorTests):
1212
    """Class for tests related to KeyError."""
1213
1214
    def test_no_sugg(self):
1215
        """No suggestion."""
1216
        self.throws('dict()["ffdsqmjklfqsd"]', KEYERROR)
1217
1218
1219
class IndexErrorTests(LookupErrorTests):
1220
    """Class for tests related to IndexError."""
1221
1222
    def test_no_sugg(self):
1223
        """No suggestion."""
1224
        self.throws('list()[2]', OUTOFRANGE)
1225
1226
1227
class SyntaxErrorTests(GetSuggestionsTests):
1228
    """Class for tests related to SyntaxError."""
1229
1230
    def test_no_error(self):
1231
        """No error."""
1232
        self.runs("1 + 2 == 2")
1233
1234
    def test_yield_return_out_of_func(self):
1235
        """yield/return needs to be in functions."""
1236
        sugg = "to indent it"
1237
        self.throws("yield 1", OUTSIDEFUNC, sugg)
1238
        self.throws("return 1", OUTSIDEFUNC, ["'sys.exit([arg])'", sugg])
1239
1240
    def test_print(self):
1241
        """print is a functions now and needs parenthesis."""
1242
        # NICE_TO_HAVE
1243
        code, new_code = 'print ""', 'print("")'
1244
        version = (3, 0)
1245
        version2 = (3, 4)
1246
        self.runs(code, up_to_version(version))
1247
        self.throws(code, INVALIDSYNTAX, [], (version, version2))
1248
        self.throws(code, INVALIDSYNTAX, [], from_version(version2))
1249
        self.runs(new_code)
1250
1251
    def test_exec(self):
1252
        """exec is a functions now and needs parenthesis."""
1253
        # NICE_TO_HAVE
1254
        code, new_code = 'exec "1"', 'exec("1")'
1255
        version = (3, 0)
1256
        version2 = (3, 4)
1257
        self.runs(code, up_to_version(version))
1258
        self.throws(code, INVALIDSYNTAX, [], (version, version2))
1259
        self.throws(code, INVALIDSYNTAX, [], from_version(version2))
1260
        self.runs(new_code)
1261
1262
    def test_old_comparison(self):
1263
        """<> comparison is removed, != always works."""
1264
        code = '1 {0} 2'
1265
        old, new = '<>', '!='
1266
        version = (3, 0)
1267
        old_code, new_code = format_str(code, old, new)
1268
        self.runs(old_code, up_to_version(version))
1269
        self.throws(
1270
            old_code,
1271
            INVALIDCOMP,
1272
            "'!='",
1273
            from_version(version),
1274
            'pypy')
1275
        self.throws(
1276
            old_code,
1277
            INVALIDSYNTAX,
1278
            "'!='",
1279
            from_version(version),
1280
            'cython')
1281
        self.runs(new_code)
1282
1283
    def test_missing_colon(self):
1284
        """Missing colon is a classic mistake."""
1285
        # NICE_TO_HAVE
1286
        code = "if True{0}\n\tpass"
1287
        bad_code, good_code = format_str(code, "", ":")
1288
        self.throws(bad_code, INVALIDSYNTAX)
1289
        self.runs(good_code)
1290
1291
    def test_simple_equal(self):
1292
        """'=' for comparison is a classic mistake."""
1293
        # NICE_TO_HAVE
1294
        code = "if 2 {0} 3:\n\tpass"
1295
        bad_code, good_code = format_str(code, "=", "==")
1296
        self.throws(bad_code, INVALIDSYNTAX)
1297
        self.runs(good_code)
1298
1299
    def test_keyword_as_identifier(self):
1300
        """Using a keyword as a variable name."""
1301
        # NICE_TO_HAVE
1302
        code = '{0} = 1'
1303
        bad_code, good_code = format_str(code, "from", "from_")
1304
        self.throws(bad_code, INVALIDSYNTAX)
1305
        self.runs(good_code)
1306
1307
    def test_increment(self):
1308
        """Trying to use '++' or '--'."""
1309
        # NICE_TO_HAVE
1310
        code = 'a = 0\na{0}'
1311
        for op in ('-', '+'):
1312
            typo, sugg = 2 * op, op + '=1'
1313
            bad_code, good_code = format_str(code, typo, sugg)
1314
            self.throws(bad_code, INVALIDSYNTAX)
1315
            self.runs(good_code)
1316
1317
    def test_wrong_bool_operator(self):
1318
        """Trying to use '&&' or '||'."""
1319
        # NICE_TO_HAVE
1320
        code = 'True {0} False'
1321
        for typo, sugg in (('&&', 'and'), ('||', 'or')):
1322
            bad_code, good_code = format_str(code, typo, sugg)
1323
            self.throws(bad_code, INVALIDSYNTAX)
1324
            self.runs(good_code)
1325
1326
    def test_import_future_not_first(self):
1327
        """Test what happens when import from __future__ is not first."""
1328
        code = 'a = 8/7\nfrom __future__ import division'
1329
        self.throws(code, FUTUREFIRST)
1330
1331
    def test_import_future_not_def(self):
1332
        """Should be 'division'."""
1333
        code = 'from __future__ import {0}'
1334
        typo, sugg = 'divisio', 'division'
1335
        bad_code, good_code = format_str(code, typo, sugg)
1336
        self.throws(bad_code, FUTFEATNOTDEF, "'" + sugg + "'")
1337
        self.runs(good_code)
1338
1339
    def test_unqualified_exec(self):
1340
        """Exec in nested functions."""
1341
        # NICE_TO_HAVE
1342
        version = (3, 0)
1343
        codes = [
1344
            "def func1():\n\tbar='1'\n\tdef func2():"
1345
            "\n\t\texec(bar)\n\tfunc2()\nfunc1()",
1346
            "def func1():\n\texec('1')\n\tdef func2():"
1347
            "\n\t\tTrue",
1348
        ]
1349
        for code in codes:
1350
            self.throws(code, UNQUALIFIED_EXEC, [], up_to_version(version))
1351
            self.runs(code, from_version(version))
1352
1353
    def test_import_star(self):
1354
        """'import *' in nested functions."""
1355
        # NICE_TO_HAVE
1356
        codes = [
1357
            "def func1():\n\tbar='1'\n\tdef func2():"
1358
            "\n\t\tfrom math import *\n\t\tTrue\n\tfunc2()\nfunc1()",
1359
            "def func1():\n\tfrom math import *"
1360
            "\n\tdef func2():\n\t\tTrue",
1361
        ]
1362
        with warnings.catch_warnings():
1363
            warnings.simplefilter("ignore", category=SyntaxWarning)
1364
            for code in codes:
1365
                self.throws(code, IMPORTSTAR, [])
1366
1367
    def test_unpack(self):
1368
        """Extended tuple unpacking does not work prior to Python 3."""
1369
        # NICE_TO_HAVE
1370
        version = (3, 0)
1371
        code = 'a, *b = (1, 2, 3)'
1372
        self.throws(code, INVALIDSYNTAX, [], up_to_version(version))
1373
        self.runs(code, from_version(version))
1374
1375
    def test_unpack2(self):
1376
        """Unpacking in function arguments was supported up to Python 3."""
1377
        # NICE_TO_HAVE
1378
        version = (3, 0)
1379
        code = 'def addpoints((x1, y1), (x2, y2)):\n\tpass'
1380
        self.runs(code, up_to_version(version))
1381
        self.throws(code, INVALIDSYNTAX, [], from_version(version))
1382
1383
    def test_nonlocal(self):
1384
        """nonlocal keyword is added in Python 3."""
1385
        # NICE_TO_HAVE
1386
        version = (3, 0)
1387
        code = 'def func():\n\tfoo = 1\n\tdef nested():\n\t\tnonlocal foo'
1388
        self.runs(code, from_version(version))
1389
        self.throws(code, INVALIDSYNTAX, [], up_to_version(version))
1390
1391
    def test_nonlocal2(self):
1392
        """nonlocal must be used only when binding exists."""
1393
        # NICE_TO_HAVE
1394
        version = (3, 0)
1395
        code = 'def func():\n\tdef nested():\n\t\tnonlocal foo'
1396
        self.throws(code, NOBINDING, [], from_version(version))
1397
        self.throws(code, INVALIDSYNTAX, [], up_to_version(version))
1398
1399
    def test_nonlocal3(self):
1400
        """nonlocal must be used only when binding to non-global exists."""
1401
        # NICE_TO_HAVE
1402
        version = (3, 0)
1403
        code = 'foo = 1\ndef func():\n\tdef nested():\n\t\tnonlocal foo'
1404
        self.throws(code, NOBINDING, [], from_version(version))
1405
        self.throws(code, INVALIDSYNTAX, [], up_to_version(version))
1406
1407
    def test_octal_literal(self):
1408
        """Syntax for octal liberals has changed."""
1409
        # NICE_TO_HAVE
1410
        version = (3, 0)
1411
        bad, good = '0720', '0o720'
1412
        self.runs(good)
1413
        self.runs(bad, up_to_version(version))
1414
        self.throws(bad, INVALIDTOKEN, [], from_version(version))
1415
1416
    def test_extended_unpacking(self):
1417
        """Extended iterable unpacking is added with Python 3."""
1418
        version = (3, 0)
1419
        code = '(a, *rest, b) = range(5)'
1420
        self.throws(bad, INVALIDSYNTAX, [], up_to_version(version))
1421
        self.runs(code, from_version(version))
1422
1423
    def test_ellipsis(self):
1424
        """Ellipsis (...) can be used anywhere in Python 3."""
1425
        version = (3, 0)
1426
        code = '...'
1427
        self.throws(bad, INVALIDSYNTAX, [], up_to_version(version))
1428
        self.runs(code, from_version(version))
1429
1430
1431
class MemoryErrorTests(GetSuggestionsTests):
1432
    """Class for tests related to MemoryError."""
1433
1434
    def test_out_of_memory(self):
1435
        """Test what happens in case of MemoryError."""
1436
        code = '[0] * 999999999999999'
1437
        self.throws(code, MEMORYERROR)
1438
1439
    def test_out_of_memory_range(self):
1440
        """Test what happens in case of MemoryError."""
1441
        code = '{0}(999999999999999)'
1442
        typo, sugg = 'range', 'xrange'
1443
        bad_code, good_code = format_str(code, typo, sugg)
1444
        self.runs(bad_code, ALL_VERSIONS, 'pypy')
1445
        version = (2, 7)
1446
        version2 = (3, 0)
1447
        self.throws(
1448
            bad_code,
1449
            OVERFLOWERR, "'" + sugg + "'",
1450
            up_to_version(version),
1451
            'cython')
1452
        self.throws(
1453
            bad_code,
1454
            MEMORYERROR, "'" + sugg + "'",
1455
            (version, version2),
1456
            'cython')
1457
        self.runs(good_code, up_to_version(version2), 'cython')
1458
        self.runs(bad_code, from_version(version2), 'cython')
1459
1460
1461
class ValueErrorTests(GetSuggestionsTests):
1462
    """Class for tests related to ValueError."""
1463
1464
    def test_too_many_values(self):
1465
        """Unpack 4 values in 3 variables."""
1466
        code = 'a, b, c = [1, 2, 3, 4]'
1467
        version = (3, 0)
1468
        self.throws(code, EXPECTEDLENGTH, [], up_to_version(version), 'pypy')
1469
        self.throws(code, TOOMANYVALUES, [], from_version(version), 'pypy')
1470
        self.throws(code, TOOMANYVALUES, [], ALL_VERSIONS, 'cython')
1471
1472
    def test_not_enough_values(self):
1473
        """Unpack 2 values in 3 variables."""
1474
        code = 'a, b, c = [1, 2]'
1475
        version = (3, 0)
1476
        self.throws(code, EXPECTEDLENGTH, [], up_to_version(version), 'pypy')
1477
        self.throws(code, NEEDMOREVALUES, [], from_version(version), 'pypy')
1478
        self.throws(code, NEEDMOREVALUES, [], ALL_VERSIONS, 'cython')
1479
1480
    def test_conversion_fails(self):
1481
        """Conversion fails."""
1482
        self.throws('int("toto")', INVALIDLITERAL)
1483
1484
    def test_math_domain(self):
1485
        """Math function used out of its domain."""
1486
        code = 'import math\nlg = math.log(-1)'
1487
        self.throws(code, MATHDOMAIN)
1488
1489
    def test_zero_len_field_in_format(self):
1490
        """Format {} is not valid before Python 2.7."""
1491
        code = '"{0}".format(0)'
1492
        old, new = '{0}', '{}'
1493
        old_code, new_code = format_str(code, old, new)
1494
        version = (2, 7)
1495
        self.runs(old_code)
1496
        self.throws(new_code, ZEROLENERROR, '{0}', up_to_version(version))
1497
        self.runs(new_code, from_version(version))
1498
1499
    def test_timedata_does_not_match(self):
1500
        """Strptime arguments are in wrong order."""
1501
        code = 'import datetime\ndatetime.datetime.strptime({0}, {1})'
1502
        timedata, timeformat = '"30 Nov 00"', '"%d %b %y"'
1503
        good_code = code.format(*(timedata, timeformat))
1504
        bad_code = code.format(*(timeformat, timedata))
1505
        self.runs(good_code)
1506
        self.throws(bad_code, TIMEDATAFORMAT,
1507
                    ['to swap value and format parameters'])
1508
1509
1510
class IOErrorTests(GetSuggestionsTests):
1511
    """Class for tests related to IOError."""
1512
1513
    def test_no_such_file(self):
1514
        """File does not exist."""
1515
        code = 'with open("doesnotexist") as f:\n\tpass'
1516
        self.throws(code, NOFILE_IO)
1517
1518
    def test_no_such_file2(self):
1519
        """File does not exist."""
1520
        code = 'os.listdir("doesnotexist")'
1521
        self.throws(code, NOFILE_OS)
1522
1523
    def test_no_such_file_user(self):
1524
        """Suggestion when one needs to expanduser."""
1525
        code = 'os.listdir("{0}")'
1526
        typo, sugg = "~", os.path.expanduser("~")
1527
        bad_code, good_code = format_str(code, typo, sugg)
1528
        self.throws(
1529
            bad_code, NOFILE_OS,
1530
            "'" + sugg + "' (calling os.path.expanduser)")
1531
        self.runs(good_code)
1532
1533
    def test_no_such_file_vars(self):
1534
        """Suggestion when one needs to expandvars."""
1535
        code = 'os.listdir("{0}")'
1536
        key = 'HOME'
1537
        typo, sugg = "$" + key, os.path.expanduser("~")
1538
        original_home = os.environ.get('HOME')
1539
        os.environ[key] = sugg
1540
        bad_code, good_code = format_str(code, typo, sugg)
1541
        self.throws(
1542
            bad_code, NOFILE_OS,
1543
            "'" + sugg + "' (calling os.path.expandvars)")
1544
        self.runs(good_code)
1545
        if original_home is None:
1546
            del os.environ[key]
1547
        else:
1548
            os.environ[key] = original_home
1549
1550
    def create_tmp_dir_with_files(self, filelist):
1551
        """Create a temporary directory with files in it."""
1552
        tmpdir = tempfile.mkdtemp()
1553
        absfiles = [os.path.join(tmpdir, f) for f in filelist]
1554
        for name in absfiles:
1555
            open(name, 'a').close()
1556
        return (tmpdir, absfiles)
1557
1558
    def test_is_dir_empty(self):
1559
        """Suggestion when file is an empty directory."""
1560
        # Create empty temp dir
1561
        tmpdir, _ = self.create_tmp_dir_with_files([])
1562
        code = 'with open("{0}") as f:\n\tpass'
1563
        bad_code, _ = format_str(code, tmpdir, "TODO")
1564
        self.throws(
1565
            bad_code, ISADIR_IO, "to add content to {0} first".format(tmpdir))
1566
        rmtree(tmpdir)
1567
1568
    def test_is_dir_small(self):
1569
        """Suggestion when file is directory with a few files."""
1570
        # Create temp dir with a few files
1571
        nb_files = 3
1572
        files = sorted([str(i) + ".txt" for i in range(nb_files)])
1573
        tmpdir, absfiles = self.create_tmp_dir_with_files(files)
1574
        code = 'with open("{0}") as f:\n\tpass'
1575
        bad_code, good_code = format_str(code, tmpdir, absfiles[0])
1576
        self.throws(
1577
            bad_code, ISADIR_IO,
1578
            "any of the 3 files in directory ('0.txt', '1.txt', '2.txt')")
1579
        self.runs(good_code)
1580
        rmtree(tmpdir)
1581
1582
    def test_is_dir_big(self):
1583
        """Suggestion when file is directory with many files."""
1584
        # Create temp dir with many files
1585
        tmpdir = tempfile.mkdtemp()
1586
        nb_files = 30
1587
        files = sorted([str(i) + ".txt" for i in range(nb_files)])
1588
        tmpdir, absfiles = self.create_tmp_dir_with_files(files)
1589
        code = 'with open("{0}") as f:\n\tpass'
1590
        bad_code, good_code = format_str(code, tmpdir, absfiles[0])
1591
        self.throws(
1592
            bad_code, ISADIR_IO,
1593
            "any of the 30 files in directory "
1594
            "('0.txt', '1.txt', '10.txt', '11.txt', etc)")
1595
        self.runs(good_code)
1596
        rmtree(tmpdir)
1597
1598
    def test_is_not_dir(self):
1599
        """Suggestion when file is not a directory."""
1600
        code = 'with open("{0}") as f:\n\tpass'
1601
        code = 'os.listdir("{0}")'
1602
        typo, sugg = __file__, os.path.dirname(__file__)
1603
        bad_code, good_code = format_str(code, typo, sugg)
1604
        self.throws(
1605
            bad_code, NOTADIR_OS,
1606
            "'" + sugg + "' (calling os.path.dirname)")
1607
        self.runs(good_code)
1608
1609
    def test_dir_is_not_empty(self):
1610
        """Suggestion when directory is not empty."""
1611
        # NICE_TO_HAVE
1612
        nb_files = 3
1613
        files = sorted([str(i) + ".txt" for i in range(nb_files)])
1614
        tmpdir, _ = self.create_tmp_dir_with_files(files)
1615
        self.throws('os.rmdir("{0}")'.format(tmpdir), DIRNOTEMPTY_OS)
1616
        rmtree(tmpdir)  # this should be the suggestion
1617
1618
1619
class AnyErrorTests(GetSuggestionsTests):
1620
    """Class for tests not related to an error type in particular."""
1621
1622
    def test_wrong_except(self):
1623
        """Test where except is badly used and thus does not catch.
1624
1625
        Common mistake : "except Exc1, Exc2" doesn't catch Exc2.
1626
        Adding parenthesis solves the issue.
1627
        """
1628
        # NICE_TO_HAVE
1629
        version = (3, 0)
1630
        raised_exc, other_exc = KeyError, TypeError
1631
        raised, other = raised_exc.__name__, other_exc.__name__
1632
        code = "try:\n\traise {0}()\nexcept {{0}}:\n\tpass".format(raised)
1633
        typo = "{0}, {1}".format(other, raised)
1634
        sugg = "({0})".format(typo)
1635
        bad1, bad2, good1, good2 = format_str(code, typo, other, sugg, raised)
1636
        self.throws(bad1, (raised_exc, None), [], up_to_version(version))
1637
        self.throws(bad1, INVALIDSYNTAX, [], from_version(version))
1638
        self.throws(bad2, (raised_exc, None))
1639
        self.runs(good1)
1640
        self.runs(good2)
1641
1642
1643
if __name__ == '__main__':
1644
    print(sys.version_info)
1645
    unittest2.main()
1646