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