Completed
Push — master ( b86d85...a3675f )
by De
01:27
created

suggest_unexpected_keywordarg_for_func()   C

Complexity

Conditions 8

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
rs 6.6666
cc 8
1
# -*- coding: utf-8
2
"""Logic to add suggestions to exceptions."""
3
import keyword
4
import difflib
5
import didyoumean_re as re
6
import itertools
7
import inspect
8
import errno
9
import os
10
import sys
11
from collections import namedtuple
12
13
14
#: Standard modules we'll consider while searching for symbols, for instance:
15
#  - NameError and the name is an attribute of a std (imported or not) module
16
#  - NameError and the name is the name of a standard (non imported) module
17
#  - ImportError and the name looks like a standard (imported or not) module
18
#  - TODO: AttributeError and the attribute is the one of a module
19
# Not that in the first case, the modules must be considered safe to import
20
# (no side-effects) but in some other cases, we only care about the names
21
# of the module and a more extended list could be used.
22
# The list is to be completed
23
# Potential candidates :
24
#  - sys.builtin_module_names
25
# https://docs.python.org/2/library/sys.html#sys.builtin_module_names
26
#  - sys.modules
27
# https://docs.python.org/2/library/sys.html#sys.modules
28
#  - pkgutil.iter_modules
29
# https://docs.python.org/2/library/pkgutil.html#pkgutil.iter_modules
30
STAND_MODULES = set(['string', 'os', 'sys', 're', 'math', 'random',
31
                     'datetime', 'timeit', 'unittest', 'itertools',
32
                     'functools', 'collections', '__future__'])
33
34
#: Almost synonyms methods that can be confused from one type to another
35
# To be completed
36
SYNONYMS_SETS = [set(['add', 'append']), set(['extend', 'update'])]
37
38
#: Maximum number of files suggested
39
MAX_NB_FILES = 4
40
41
#: Message to suggest not using recursion
42
AVOID_REC_MSG = \
43
    "to avoid recursion (cf " \
44
    "http://neopythonic.blogspot.fr/2009/04/tail-recursion-elimination.html)"
45
#: Messages for functions removed from one version to another
46
APPLY_REMOVED_MSG = "to call the function directly (`apply` is deprecated " \
47
    "since Python 2.3, removed since Python 3)"
48
BUFFER_REMOVED_MSG = '"memoryview" (`buffer` has been removed " \
49
    "since Python 3)'
50
CMP_REMOVED_MSG = "to use comparison operators (`cmp` is removed since " \
51
    "Python 3 but you can define `def cmp(a, b): return (a > b) - (a < b)` " \
52
    "if needed)"
53
CMP_ARG_REMOVED_MSG = 'to use "key" (`cmp` has been replaced by `key` ' \
54
    "since Python 3 - `functools.cmp_to_key` provides a convenient way " \
55
    "to convert cmp function to key function)"
56
EXC_ATTR_REMOVED_MSG = 'to use "sys.exc_info()" returning a tuple ' \
57
    'of the form (type, value, traceback) ("exc_type", "exc_value" and ' \
58
    '"exc_traceback" are removed from sys since Python 3)'
59
LONG_REMOVED_MSG = 'to use "int" (since Python 3, there is only one ' \
60
    'integer type: `int`)'
61
MEMVIEW_ADDED_MSG = '"buffer" (`memoryview` is added in Python 2.7 and " \
62
    "completely replaces `buffer` since Python 3)'
63
RELOAD_REMOVED_MSG = '"importlib.reload" or "imp.reload" (`reload` is " \
64
    "removed since Python 3)'
65
STDERR_REMOVED_MSG = '"Exception" (`StandardError` has been removed since " \
66
    "Python 3)'
67
NO_KEYWORD_ARG_MSG = "use positional arguments (functions written in C \
68
    do not accept keyword arguments, only positional arguments)"
69
70
71
# Helper function for string manipulation
72
def quote(string):
73
    """Surround string with single quotes."""
74
    return "'{0}'".format(string)
75
76
77
def get_close_matches(word, possibilities):
78
    """
79
    Return a list of the best "good enough" matches.
80
81
    Wrapper around difflib.get_close_matches() to be able to
82
    change default values or implementation details easily.
83
    """
84
    return [w
85
            for w in difflib.get_close_matches(word, possibilities, 3, 0.7)
86
            if w != word]
87
88
89
def get_suggestion_string(sugg):
90
    """Return the suggestion list as a string."""
91
    sugg = list(sugg)
92
    return ". Did you mean " + ", ".join(sugg) + "?" if sugg else ""
93
94
95
# Helper functions for code introspection
96
def subclasses_wrapper(klass):
97
    """Wrapper around __subclass__ as it is not as easy as it should."""
98
    method = getattr(klass, '__subclasses__', None)
99
    if method is None:
100
        return []
101
    try:
102
        return method()
103
    except TypeError:
104
        try:
105
            return method(klass)
106
        except TypeError:
107
            return []
108
109
110
def get_subclasses(klass):
111
    """Get the subclasses of a class.
112
113
    Get the set of direct/indirect subclasses of a class including itself.
114
    """
115
    subclasses = set(subclasses_wrapper(klass))
116
    for derived in set(subclasses):
117
        subclasses.update(get_subclasses(derived))
118
    subclasses.add(klass)
119
    return subclasses
120
121
122
def get_types_for_str_using_inheritance(name):
123
    """Get types corresponding to a string name.
124
125
    This goes through all defined classes. Therefore, it :
126
    - does not include old style classes on Python 2.x
127
    - is to be called as late as possible to ensure wanted type is defined.
128
    """
129
    return set(c for c in get_subclasses(object) if c.__name__ == name)
130
131
132
def get_types_for_str_using_names(name, frame):
133
    """Get types corresponding to a string name using names in frame.
134
135
    This does not find everything as builtin types for instance may not
136
    be in the names.
137
    """
138
    return set(obj
139
               for obj, _ in get_objects_in_frame(frame).get(name, [])
140
               if inspect.isclass(obj) and obj.__name__ == name)
141
142
143
def get_types_for_str(tp_name, frame):
144
    """Get a list of candidate types from a string.
145
146
    String corresponds to the tp_name as described in :
147
    https://docs.python.org/2/c-api/typeobj.html#c.PyTypeObject.tp_name
148
    as it is the name used in exception messages. It may include full path
149
    with module, subpackage, package but this is just removed in current
150
    implementation to search only based on the type name.
151
152
    Lookup uses both class hierarchy and name lookup as the first may miss
153
    old style classes on Python 2 and second does find them.
154
    Just like get_types_for_str_using_inheritance, this needs to be called
155
    as late as possible but because it requires a frame, there is not much
156
    choice anyway.
157
    """
158
    name = tp_name.split('.')[-1]
159
    res = set.union(
160
        get_types_for_str_using_inheritance(name),
161
        get_types_for_str_using_names(name, frame))
162
    assert all(inspect.isclass(t) and t.__name__ == name for t in res)
163
    return res
164
165
166
def merge_dict(*dicts):
167
    """Merge dicts and return a dictionnary mapping key to list of values.
168
169
    Order of the values corresponds to the order of the original dicts.
170
    """
171
    ret = dict()
172
    for dict_ in dicts:
173
        for key, val in dict_.items():
174
            ret.setdefault(key, []).append(val)
175
    return ret
176
177
ScopedObj = namedtuple('ScopedObj', 'obj scope')
178
179
180
def add_scope_to_dict(dict_, scope):
181
    """Convert name:obj dict to name:ScopedObj(obj,scope) dict."""
182
    return dict((k, ScopedObj(v, scope)) for k, v in dict_.items())
183
184
185
def get_objects_in_frame(frame):
186
    """Get objects defined in a given frame.
187
188
    This includes variable, types, builtins, etc.
189
    The function returns a dictionnary mapping names to a (non empty)
190
    list of ScopedObj objects in the order following the LEGB Rule.
191
    """
192
    # https://www.python.org/dev/peps/pep-0227/ PEP227 Statically Nested Scopes
193
    # "Under this proposal, it will not be possible to gain dictionary-style
194
    #      access to all visible scopes."
195
    # https://www.python.org/dev/peps/pep-3104/ PEP 3104 Access to Names in
196
    #      Outer Scopes
197
    # LEGB Rule : missing E (enclosing) at the moment.
198
    # I'm not sure if it can be fixed but if it can, suggestions
199
    # tagged TODO_ENCLOSING could be implemented (and tested).
200
    return merge_dict(
201
        add_scope_to_dict(frame.f_locals, 'local'),
202
        add_scope_to_dict(frame.f_globals, 'global'),
203
        add_scope_to_dict(frame.f_builtins, 'builtin'),
204
    )
205
206
207
def import_from_frame(module_name, frame):
208
    """Wrapper around import to use information from frame."""
209
    if frame is None:
210
        return None
211
    return __import__(
212
        module_name,
213
        frame.f_globals,
214
        frame.f_locals)
215
216
217
# To be used in `get_suggestions_for_exception`.
218
SUGGESTION_FUNCTIONS = dict()
219
220
221
def register_suggestion_for(error_type, regex):
222
    """Decorator to register a function to be called to get suggestions.
223
224
    Parameters correspond to the fact that the registration is done for a
225
    specific error type and if the error message matches a given regex
226
    (if the regex is None, the error message is assumed to match before being
227
    retrieved).
228
229
    The decorated function is expected to yield any number (0 included) of
230
    suggestions (as string).
231
    The parameters are: (value, frame, groups):
232
     - value: Exception object
233
     - frame: Last frame of the traceback (may be None when the traceback is
234
        None which happens only in edge cases)
235
     - groups: Groups from the error message matched by the error message.
236
    """
237
    def internal_decorator(func):
238
        def registered_function(value, frame):
239
            if regex is None:
240
                return func(value, frame, [])
241
            error_msg = value.args[0]
242
            match = re.match(regex, error_msg)
243
            if match:
244
                return func(value, frame, match.groups())
245
            return []
246
        SUGGESTION_FUNCTIONS.setdefault(error_type, []) \
247
            .append(registered_function)
248
        return func  # return original function
249
    return internal_decorator
250
251
252
# Functions related to NameError
253
@register_suggestion_for(NameError, re.VARREFBEFOREASSIGN_RE)
254
@register_suggestion_for(NameError, re.NAMENOTDEFINED_RE)
255
def suggest_name_not_defined(value, frame, groups):
256
    """Get the suggestions for name in case of NameError."""
257
    del value  # unused param
258
    name, = groups
259
    objs = get_objects_in_frame(frame)
260
    return itertools.chain(
261
        suggest_name_as_attribute(name, objs),
262
        suggest_name_as_standard_module(name),
263
        suggest_name_as_name_typo(name, objs),
264
        suggest_name_as_keyword_typo(name),
265
        suggest_name_as_missing_import(name, objs, frame),
266
        suggest_name_as_special_case(name))
267
268
269
def suggest_name_as_attribute(name, objdict):
270
    """Suggest that name could be an attribute of an object.
271
272
    Example: 'do_stuff()' -> 'self.do_stuff()'.
273
    """
274
    for nameobj, objs in objdict.items():
275
        prev_scope = None
276
        for obj, scope in objs:
277
            if hasattr(obj, name):
278
                yield quote(nameobj + '.' + name) + \
279
                    ('' if prev_scope is None else
280
                     ' ({0} hidden by {1})'.format(scope, prev_scope))
281
                break
282
            prev_scope = scope
283
284
285
def suggest_name_as_missing_import(name, objdict, frame):
286
    """Suggest that name could come from missing import.
287
288
    Example: 'foo' -> 'import mod, mod.foo'.
289
    """
290
    for mod in STAND_MODULES:
291
        if mod not in objdict and name in dir(import_from_frame(mod, frame)):
292
            yield "'{0}' from {1} (not imported)".format(name, mod)
293
294
295
def suggest_name_as_standard_module(name):
296
    """Suggest that name could be a non-imported standard module.
297
298
    Example: 'os.whatever' -> 'import os' and then 'os.whatever'.
299
    """
300
    if name in STAND_MODULES:
301
        yield 'to import {0} first'.format(name)
302
303
304
def suggest_name_as_name_typo(name, objdict):
305
    """Suggest that name could be a typo (misspelled existing name).
306
307
    Example: 'foobaf' -> 'foobar'.
308
    """
309
    for name in get_close_matches(name, objdict.keys()):
310
        yield quote(name) + ' (' + objdict[name][0].scope + ')'
311
312
313
def suggest_name_as_keyword_typo(name):
314
    """Suggest that name could be a typo (misspelled keyword).
315
316
    Example: 'yieldd' -> 'yield'.
317
    """
318
    for name in get_close_matches(name, keyword.kwlist):
319
        yield quote(name) + " (keyword)"
320
321
322
def suggest_name_as_special_case(name):
323
    """Suggest that name could be handled in a special way."""
324
    special_cases = {
325
        # Imaginary unit is '1j' in Python
326
        'i': quote('1j') + " (imaginary unit)",
327
        'j': quote('1j') + " (imaginary unit)",
328
        # Shell commands entered in interpreter
329
        'pwd': quote('os.getcwd()'),
330
        'ls': quote('os.listdir(os.getcwd())'),
331
        'cd': quote('os.chdir(path)'),
332
        'rm': "'os.remove(filename)', 'shutil.rmtree(dir)' for recursive",
333
        # Function removed from Python
334
        'apply': APPLY_REMOVED_MSG,
335
        'buffer': BUFFER_REMOVED_MSG,
336
        'cmp': CMP_REMOVED_MSG,
337
        'long': LONG_REMOVED_MSG,
338
        'memoryview': MEMVIEW_ADDED_MSG,
339
        'reload': RELOAD_REMOVED_MSG,
340
        'StandardError': STDERR_REMOVED_MSG,
341
    }
342
    result = special_cases.get(name)
343
    if result is not None:
344
        yield result
345
346
347
# Functions related to AttributeError
348
@register_suggestion_for(AttributeError, re.ATTRIBUTEERROR_RE)
349
@register_suggestion_for(TypeError, re.ATTRIBUTEERROR_RE)
350
def suggest_attribute_error(value, frame, groups):
351
    """Get suggestions in case of ATTRIBUTEERROR."""
352
    del value  # unused param
353
    type_str, attr = groups
354
    return get_attribute_suggestions(type_str, attr, frame)
355
356
357
@register_suggestion_for(AttributeError, re.MODULEHASNOATTRIBUTE_RE)
358
def suggest_module_has_no_attr(value, frame, groups):
359
    """Get suggestions in case of MODULEHASNOATTRIBUTE."""
360
    del value  # unused param
361
    _, attr = groups  # name ignored for the time being
362
    return get_attribute_suggestions('module', attr, frame)
363
364
365
def get_attribute_suggestions(type_str, attribute, frame):
366
    """Get the suggestions closest to the attribute name for a given type."""
367
    types = get_types_for_str(type_str, frame)
368
    attributes = set(a for t in types for a in dir(t))
369
    if type_str == 'module':
370
        # For module, we manage to get the corresponding 'module' type
371
        # but the type doesn't bring much information about its content.
372
        # A hacky way to do so is to assume that the exception was something
373
        # like 'module_name.attribute' so that we can actually find the module
374
        # based on the name. Eventually, we check that the found object is a
375
        # module indeed. This is not failproof but it brings a whole lot of
376
        # interesting suggestions and the (minimal) risk is to have invalid
377
        # suggestions.
378
        module_name = frame.f_code.co_names[0]
379
        objs = get_objects_in_frame(frame)
380
        mod = objs[module_name][0].obj
381
        if inspect.ismodule(mod):
382
            attributes = set(dir(mod))
383
384
    return itertools.chain(
385
        suggest_attribute_as_builtin(attribute, type_str, frame),
386
        suggest_attribute_alternative(attribute, type_str, attributes),
387
        suggest_attribute_synonyms(attribute, attributes),
388
        suggest_attribute_as_typo(attribute, attributes),
389
        suggest_attribute_as_special_case(attribute))
390
391
392
def suggest_attribute_as_builtin(attribute, type_str, frame):
393
    """Suggest that a builtin was used as an attribute.
394
395
    Example: 'lst.len()' -> 'len(lst)'.
396
    """
397
    obj = frame.f_builtins.get(attribute)
398
    if obj is not None and '__call__' in dir(obj):
399
        yield quote(attribute + '(' + type_str + ')')
400
401
402
def suggest_attribute_alternative(attribute, type_str, attributes):
403
    """Suggest alternative to the non-found attribute."""
404
    is_iterable = '__iter__' in attributes or \
405
                  ('__getitem__' in attributes and '__len__' in attributes)
406
    if attribute == 'has_key' and '__contains__' in attributes:
407
        yield quote('key in ' + type_str) + ' (has_key is removed)'
408
    elif attribute == 'get' and '__getitem__' in attributes:
409
        yield quote('obj[key]') + \
410
            ' with a len() check or try: except: KeyError or IndexError'
411
    elif attribute in ('__setitem__', '__delitem__'):
412
        if is_iterable:
413
            msg = 'convert to list to edit the list'
414
            if 'join' in attributes:
415
                msg += ' and use "join()" on the list'
416
            yield msg
417
    elif attribute == '__getitem__':
418
        if '__call__' in attributes:
419
            yield quote(type_str + '(value)')
420
        if is_iterable:
421
            yield 'convert to list first or use the iterator protocol to ' \
422
                    'get the different elements'
423
    elif attribute == '__call__':
424
        if '__getitem__' in attributes:
425
            yield quote(type_str + '[value]')
426
    elif attribute == '__len__':
427
        if is_iterable:
428
            yield quote('len(list(' + type_str + '))')
429
    elif attribute == 'join':
430
        if is_iterable:
431
            yield quote('my_string.join(' + type_str + ')')
432
    elif attribute == '__or__':
433
        if '__pow__' in attributes:
434
            yield quote('val1 ** val2')
435
    elif attribute == '__index__':
436
        if '__len__' in attributes:
437
            yield quote('len(' + type_str + ')')
438
        if type_str in ('str', 'float'):
439
            yield quote('int(' + type_str + ')')
440
            if type_str == 'float' and sys.version_info >= (3, 0):
441
                # These methods return 'float' before Python 3
442
                yield quote('math.floor(' + type_str + ')')
443
                yield quote('math.ceil(' + type_str + ')')
444
445
446
def suggest_attribute_synonyms(attribute, attributes):
447
    """Suggest that a method with a similar meaning was used.
448
449
    Example: 'lst.add(e)' -> 'lst.append(e)'.
450
    """
451
    for set_sub in SYNONYMS_SETS:
452
        if attribute in set_sub:
453
            for syn in set_sub & attributes:
454
                yield quote(syn)
455
456
457
def suggest_attribute_as_typo(attribute, attributes):
458
    """Suggest the attribute could be a typo.
459
460
    Example: 'a.do_baf()' -> 'a.do_bar()'.
461
    """
462
    for name in get_close_matches(attribute, attributes):
463
        # Handle Private name mangling
464
        if name.startswith('_') and '__' in name and not name.endswith('__'):
465
            yield quote(name) + ' (but it is supposed to be private)'
466
        else:
467
            yield quote(name)
468
469
470
def suggest_attribute_as_special_case(attribute):
471
    """Suggest that attribute could be handled in a specific way."""
472
    special_cases = {
473
        'exc_type': EXC_ATTR_REMOVED_MSG,
474
        'exc_value': EXC_ATTR_REMOVED_MSG,
475
        'exc_traceback': EXC_ATTR_REMOVED_MSG,
476
    }
477
    result = special_cases.get(attribute)
478
    if result is not None:
479
        yield result
480
481
482
# Functions related to ImportError
483
@register_suggestion_for(ImportError, re.NOMODULE_RE)
484
def suggest_no_module(value, frame, groups):
485
    """Get the suggestions closest to the failing module import.
486
487
    Example: 'import maths' -> 'import math'.
488
    """
489
    del value, frame  # unused param
490
    module_str, = groups
491
    for name in get_close_matches(module_str, STAND_MODULES):
492
        yield quote(name)
493
494
495
@register_suggestion_for(ImportError, re.CANNOTIMPORT_RE)
496
def suggest_cannot_import(value, frame, groups):
497
    """Get the suggestions closest to the failing import."""
498
    del value  # unused param
499
    imported_name, = groups
500
    module_name = frame.f_code.co_names[0]
501
    return itertools.chain(
502
        suggest_imported_name_as_typo(imported_name, module_name, frame),
503
        suggest_import_from_module(imported_name, frame))
504
505
506
def suggest_imported_name_as_typo(imported_name, module_name, frame):
507
    """Suggest that imported name could be a typo from actual name in module.
508
509
    Example: 'from math import pie' -> 'from math import pi'.
510
    """
511
    dir_mod = dir(import_from_frame(module_name, frame))
512
    for name in get_close_matches(imported_name, dir_mod):
513
        yield quote(name)
514
515
516
def suggest_import_from_module(imported_name, frame):
517
    """Suggest than name could be found in a standard module.
518
519
    Example: 'from itertools import pi' -> 'from math import pi'.
520
    """
521
    for mod in STAND_MODULES:
522
        if imported_name in dir(import_from_frame(mod, frame)):
523
            yield quote('from {0} import {1}'.format(mod, imported_name))
524
525
526
# Functions related to TypeError
527
def suggest_feature_not_supported(attr, type_str, frame):
528
    """Get suggestion for unsupported feature."""
529
    # 'Object does not support <feature>' exceptions
530
    # can be somehow seen as attribute errors for magic
531
    # methods except for the fact that we do not want to
532
    # have any fuzzy logic on the magic method name.
533
    # Also, we want to suggest the implementation of the
534
    # missing method (if is it not on a builtin object).
535
    types = get_types_for_str(type_str, frame)
536
    attributes = set(a for t in types for a in dir(t))
537
    for s in suggest_attribute_alternative(attr, type_str, attributes):
538
        yield s
539
    if type_str not in frame.f_builtins and \
540
            type_str not in ('function', 'generator'):
541
        yield 'implement "' + attr + '" on ' + type_str
542
543
544
@register_suggestion_for(TypeError, re.UNSUBSCRIPTABLE_RE)
545
def suggest_unsubscriptable(value, frame, groups):
546
    """Get suggestions in case of UNSUBSCRIPTABLE error."""
547
    del value  # unused param
548
    type_str, = groups
549
    return suggest_feature_not_supported('__getitem__', type_str, frame)
550
551
552
@register_suggestion_for(TypeError, re.NOT_CALLABLE_RE)
553
def suggest_not_callable(value, frame, groups):
554
    """Get suggestions in case of NOT_CALLABLE error."""
555
    del value  # unused param
556
    type_str, = groups
557
    return suggest_feature_not_supported('__call__', type_str, frame)
558
559
560
@register_suggestion_for(TypeError, re.OBJ_DOES_NOT_SUPPORT_RE)
561
def suggest_obj_does_not_support(value, frame, groups):
562
    """Get suggestions in case of OBJ DOES NOT SUPPORT error."""
563
    del value  # unused param
564
    type_str, feature = groups
565
    FEATURES = {
566
        'indexing': '__getitem__',
567
        'item assignment': '__setitem__',
568
        'item deletion': '__delitem__',
569
    }
570
    attr = FEATURES.get(feature)
571
    if attr is None:
572
        return []
573
    return suggest_feature_not_supported(attr, type_str, frame)
574
575
576
@register_suggestion_for(TypeError, re.OBJECT_HAS_NO_FUNC_RE)
577
def suggest_obj_has_no(value, frame, groups):
578
    """Get suggestions in case of OBJECT_HAS_NO_FUNC."""
579
    del value  # unused param
580
    type_str, feature = groups
581
    if feature in ('length', 'len'):
582
        return suggest_feature_not_supported('__len__', type_str, frame)
583
    return []
584
585
586
@register_suggestion_for(TypeError, re.BAD_OPERAND_UNARY_RE)
587
def suggest_bad_operand_for_unary(value, frame, groups):
588
    """Get suggestions for BAD_OPERAND_UNARY."""
589
    del value  # unused param
590
    unary, type_str = groups
591
    UNARY_OPS = {
592
        '+': '__pos__',
593
        'pos': '__pos__',
594
        '-': '__neg__',
595
        'neg': '__neg__',
596
        '~': '__invert__',
597
        'abs()': '__abs__',
598
        'abs': '__abs__',
599
    }
600
    attr = UNARY_OPS.get(unary)
601
    if attr is None:
602
        return []
603
    return suggest_feature_not_supported(attr, type_str, frame)
604
605
606
@register_suggestion_for(TypeError, re.UNSUPPORTED_OP_RE)
607
def suggest_unsupported_op(value, frame, groups):
608
    """Get suggestions for UNSUPPORTED_OP_RE."""
609
    del value  # unused param
610
    binary, type1, type2 = groups
611
    BINARY_OPS = {
612
        '^': '__or__',
613
    }
614
    attr = BINARY_OPS.get(binary)
615
    if attr is None:
616
        return []
617
    # Suggestion is based on first type which may not be the best
618
    del type2  # unused value
619
    return suggest_feature_not_supported(attr, type1, frame)
620
621
622
@register_suggestion_for(TypeError, re.CANNOT_BE_INTERPRETED_INT_RE)
623
@register_suggestion_for(TypeError, re.INTEGER_EXPECTED_GOT_RE)
624
@register_suggestion_for(TypeError, re.INDICES_MUST_BE_INT_RE)
625
def suggest_integer_type_expected(value, frame, groups):
626
    """Get suggestions when an int is wanted."""
627
    del value  # unused param
628
    type_str, = groups
629
    return suggest_feature_not_supported('__index__', type_str, frame)
630
631
632
def get_func_by_name(func_name, frame):
633
    """Get the function with the given name in the frame."""
634
    objs = get_objects_in_frame(frame)
635
    # Trying to fetch reachable objects: getting objects and attributes
636
    # for objects. We would go deeper (with a fixed point algorithm) but
637
    # it doesn't seem to be worth it. In any case, we'll be missing a few
638
    # possible functions.
639
    objects = [o.obj for lst in objs.values() for o in lst]
640
    for obj in list(objects):
641
        for a in dir(obj):
642
            attr = getattr(obj, a, None)
643
            if attr is not None:
644
                objects.append(attr)
645
    # Then, we filter for function with the correct name (the name being the
646
    # name on the function object which is not always the same from the
647
    # namespace).
648
    return [func
649
            for func in objects
650
            if getattr(func, '__name__', None) == func_name]
651
652
653
def suggest_unexpected_keywordarg_for_func(kw_arg, func_name, frame):
654
    """Get suggestions in case of unexpected keyword argument."""
655
    functions = get_func_by_name(func_name, frame)
656
    func_codes = [f.__code__ for f in functions if hasattr(f, '__code__')]
657
    args = set([var for func in func_codes for var in func.co_varnames])
658
    for arg_name in get_close_matches(kw_arg, args):
659
        yield quote(arg_name)
660
    if kw_arg == 'cmp' and 'key' in args:
661
        yield CMP_ARG_REMOVED_MSG
662
663
664
@register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG_RE)
665
def suggest_unexpected_keywordarg(value, frame, groups):
666
    """Get suggestions in case of UNEXPECTED_KEYWORDARG error."""
667
    del value  # unused param
668
    func_name, kw_arg = groups
669
    return suggest_unexpected_keywordarg_for_func(kw_arg, func_name, frame)
670
671
672
@register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG4_RE)
673
def suggest_unexpected_keywordarg(value, frame, groups):
674
    """Get suggestions in case of UNEXPECTED_KEYWORDARG4 error."""
675
    del value  # unused param
676
    kw_arg, func_name = groups
677
    return suggest_unexpected_keywordarg_for_func(kw_arg, func_name, frame)
678
679
680
@register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG2_RE)
681
def suggest_unexpected_keywordarg2(value, frame, groups):
682
    """Get suggestions in case of UNEXPECTED_KEYWORDARG2 error."""
683
    del value, frame  # unused param
684
    kw_arg, = groups
685
    if kw_arg == 'cmp':
686
        yield CMP_ARG_REMOVED_MSG
687
688
689
@register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG3_RE)
690
def suggest_unexpected_keywordarg3(value, frame, groups):
691
    """Get suggestions in case of UNEXPECTED_KEYWORDARG2 error."""
692
    del value, frame  # unused param
693
    func_name, = groups
694
    del func_name  # unused value
695
    return []  # no implementation so far
696
697
698
@register_suggestion_for(TypeError, re.NB_ARG_RE)
699
def suggest_nb_arg(value, frame, groups):
700
    """Get suggestions in case of NB ARGUMENT error."""
701
    del value  # unused param
702
    func_name, expected, given = groups
703
    expect_nb = 0 if expected == 'no' else int(expected)
704
    given_nb = int(given)
705
    objs = get_objects_in_frame(frame)
706
    del expect_nb, given_nb, objs, func_name  # for later
707
    return []
708
709
710
@register_suggestion_for(TypeError, re.FUNC_TAKES_NO_KEYWORDARG_RE)
711
def suggest_func_no_kw_arg(value, frame, groups):
712
    """Get suggestions for FUNC_TAKES_NO_KEYWORDARG_RE."""
713
    # C-Level functions don't have actual names for their arguments.
714
    # Therefore, trying to use them with keyword arguments leads to
715
    # errors but using them with positional arguments just work fine.
716
    # This behavior definitly deserves some suggestion.
717
    # More reading:
718
    # http://stackoverflow.com/questions/24463202/typeerror-get-takes-no-keyword-arguments
719
    # https://www.python.org/dev/peps/pep-0457/
720
    # https://www.python.org/dev/peps/pep-0436/#functions-with-positional-only-parameters
721
    # Note: a proper implementation of this function would:
722
    #  - retrieve the function object using the function name
723
    #  - check that the function does accept arguments but does not
724
    # accept keyword arguments before yielding the suggestion.
725
    # Unfortunately, introspection of builtin function is not possible as per
726
    # http://bugs.python.org/issue1748064 . Thus, the only thing we can look
727
    # for is if a function has no __code__ attribute.
728
    func_name, = groups
729
    functions = get_func_by_name(func_name, frame)
730
    if any([not hasattr(f, '__code__') for f in functions]):
731
        yield NO_KEYWORD_ARG_MSG
732
733
734
# Functions related to ValueError
735
@register_suggestion_for(ValueError, re.ZERO_LEN_FIELD_RE)
736
def suggest_zero_len_field(value, frame, groups):
737
    """Get suggestions in case of ZERO_LEN_FIELD."""
738
    del value, frame, groups  # unused param
739
    yield '{0}'
740
741
742
@register_suggestion_for(ValueError, re.TIME_DATA_DOES_NOT_MATCH_FORMAT_RE)
743
def suggest_time_data_is_wrong(value, frame, groups):
744
    """Get suggestions in case of TIME_DATA_DOES_NOT_MATCH_FORMAT_RE."""
745
    del value, frame  # unused param
746
    timedata, timeformat = groups
747
    if timedata.count('%') > timeformat.count('%%'):
748
        yield "to swap value and format parameters"
749
750
751
# Functions related to SyntaxError
752
@register_suggestion_for(SyntaxError, re.OUTSIDE_FUNCTION_RE)
753
def suggest_outside_func_error(value, frame, groups):
754
    """Get suggestions in case of OUTSIDE_FUNCTION error."""
755
    del value, frame  # unused param
756
    yield "to indent it"
757
    word, = groups
758
    if word == 'return':
759
        yield "'sys.exit([arg])'"
760
761
762
@register_suggestion_for(SyntaxError, re.FUTURE_FEATURE_NOT_DEF_RE)
763
def suggest_future_feature(value, frame, groups):
764
    """Get suggestions in case of FUTURE_FEATURE_NOT_DEF error."""
765
    del value  # unused param
766
    feature, = groups
767
    return suggest_imported_name_as_typo(feature, '__future__', frame)
768
769
770
@register_suggestion_for(SyntaxError, re.INVALID_COMP_RE)
771
def suggest_invalid_comp(value, frame, groups):
772
    """Get suggestions in case of INVALID_COMP error."""
773
    del value, frame, groups  # unused param
774
    yield quote('!=')
775
776
777
@register_suggestion_for(SyntaxError, re.NO_BINDING_NONLOCAL_RE)
778
def suggest_no_binding_for_nonlocal(value, frame, groups):
779
    """Get suggestions in case of NO BINDING FOR NONLOCAL."""
780
    del value  # unused param
781
    name, = groups
782
    objs = get_objects_in_frame(frame).get(name, [])
783
    for _, scope in objs:
784
        if scope == 'global':
785
            # TODO_ENCLOSING: suggest close matches for enclosing
786
            yield quote('global ' + name)
787
788
789
@register_suggestion_for(SyntaxError, re.INVALID_SYNTAX_RE)
790
def suggest_invalid_syntax(value, frame, groups):
791
    """Get suggestions in case of INVALID_SYNTAX error."""
792
    del frame, groups  # unused param
793
    alternatives = {
794
        '<>': '!=',
795
        '&&': 'and',
796
        '||': 'or',
797
    }
798
    offset = value.offset
799
    if value.offset is not None:
800
        for shift in (0, 1):
801
            offset = value.offset + shift
802
            two_last = value.text[offset - 2:offset]
803
            alt = alternatives.get(two_last)
804
            if alt is not None:
805
                yield quote(alt)
806
                break
807
808
809
# Functions related to MemoryError
810
@register_suggestion_for(MemoryError, None)
811
def get_memory_error_sugg(value, frame, groups):
812
    """Get suggestions for MemoryError exception."""
813
    del value, groups  # unused param
814
    objs = get_objects_in_frame(frame)
815
    return itertools.chain.from_iterable(
816
        suggest_memory_friendly_equi(name, objs)
817
        for name in frame.f_code.co_names)
818
819
820
# Functions related to OverflowError
821
@register_suggestion_for(OverflowError, re.RESULT_TOO_MANY_ITEMS_RE)
822
def suggest_too_many_items(value, frame, groups):
823
    """Suggest for TOO_MANY_ITEMS error."""
824
    del value  # unused param
825
    func, = groups
826
    objs = get_objects_in_frame(frame)
827
    return suggest_memory_friendly_equi(func, objs)
828
829
830
def suggest_memory_friendly_equi(name, objs):
831
    """Suggest name of a memory friendly equivalent for a function."""
832
    suggs = {'range': ['xrange']}
833
    return [quote(s) for s in suggs.get(name, []) if s in objs]
834
835
836
# Functions related to RuntimeError
837
@register_suggestion_for(RuntimeError, re.MAX_RECURSION_DEPTH_RE)
838
def suggest_max_resursion_depth(value, frame, groups):
839
    """Suggest for MAX_RECURSION_DEPTH error."""
840
    # this is the real solution, make it the first suggestion
841
    del value, frame, groups  # unused param
842
    yield AVOID_REC_MSG
843
    yield "increase the limit with " \
844
          "`sys.setrecursionlimit(limit)` (current value" \
845
          " is %d)" % sys.getrecursionlimit()
846
847
848
# Functions related to IOError/OSError
849
@register_suggestion_for((IOError, OSError), None)
850
def get_io_os_error_sugg(value, frame, groups):
851
    """Get suggestions for IOError/OSError exception."""
852
    # https://www.python.org/dev/peps/pep-3151/
853
    del frame, groups  # unused param
854
    err, _ = value.args
855
    errnos = {
856
        errno.ENOENT: suggest_if_file_does_not_exist,
857
        errno.ENOTDIR: suggest_if_file_is_not_dir,
858
        errno.EISDIR: suggest_if_file_is_dir,
859
    }
860
    return errnos.get(err, lambda x: [])(value)
861
862
863
def suggest_if_file_does_not_exist(value):
864
    """Get suggestions when a file does not exist."""
865
    # TODO: Add fuzzy match
866
    filename = value.filename
867
    for func, name in (
868
            (os.path.expanduser, 'os.path.expanduser'),
869
            (os.path.expandvars, 'os.path.expandvars')):
870
        expanded = func(filename)
871
        if os.path.exists(expanded) and filename != expanded:
872
            yield quote(expanded) + " (calling " + name + ")"
873
874
875
def suggest_if_file_is_not_dir(value):
876
    """Get suggestions when a file should have been a dir and is not."""
877
    filename = value.filename
878
    yield quote(os.path.dirname(filename)) + " (calling os.path.dirname)"
879
880
881
def suggest_if_file_is_dir(value):
882
    """Get suggestions when a file is a dir and should not."""
883
    filename = value.filename
884
    listdir = sorted(os.listdir(filename))
885
    if listdir:
886
        trunc_l = listdir[:MAX_NB_FILES]
887
        truncated = listdir != trunc_l
888
        filelist = [quote(f) for f in trunc_l] + (["etc"] if truncated else [])
889
        yield "any of the {0} files in directory ({1})".format(
890
            len(listdir), ", ".join(filelist))
891
    else:
892
        yield "to add content to {0} first".format(filename)
893
894
895
def get_suggestions_for_exception(value, traceback):
896
    """Get suggestions for an exception."""
897
    frame = get_last_frame(traceback)
898
    return itertools.chain.from_iterable(
899
            func(value, frame)
900
            for error_type, functions in SUGGESTION_FUNCTIONS.items()
901
            if isinstance(value, error_type)
902
            for func in functions)
903
904
905
def add_string_to_exception(value, string):
906
    """Add string to the exception parameter."""
907
    # The point is to have the string visible when the exception is printed
908
    # or converted to string - may it be via `str()`, `repr()` or when the
909
    # exception is uncaught and displayed (which seems to use `str()`).
910
    # In an ideal world, one just needs to update `args` but apparently it
911
    # is not enough for SyntaxError, IOError, etc where other
912
    # attributes (`msg`, `strerror`, `reason`, etc) are to be updated too
913
    # (for `str()`, not for `repr()`).
914
    # Also, elements in args might not be strings or args might me empty
915
    # so we add to the first string and add the element otherwise.
916
    assert type(value.args) == tuple
917
    if string:
918
        lst_args = list(value.args)
919
        for i, arg in enumerate(lst_args):
920
            if isinstance(arg, str):
921
                lst_args[i] = arg + string
922
                break
923
        else:
924
            # if no string arg, add the string anyway
925
            lst_args.append(string)
926
        value.args = tuple(lst_args)
927
        for attr in ['msg', 'strerror', 'reason']:
928
            attrval = getattr(value, attr, None)
929
            if attrval is not None:
930
                setattr(value, attr, attrval + string)
931
932
933
def get_last_frame(traceback):
934
    """Extract last frame from a traceback."""
935
    # In some rare case, the given traceback might be None
936
    if traceback is None:
937
        return None
938
    while traceback.tb_next:
939
        traceback = traceback.tb_next
940
    return traceback.tb_frame
941
942
943
def add_suggestions_to_exception(type_, value, traceback):
944
    """Add suggestion to an exception.
945
946
    Arguments are such as provided by sys.exc_info().
947
    """
948
    assert isinstance(value, type_)
949
    add_string_to_exception(
950
        value,
951
        get_suggestion_string(
952
            get_suggestions_for_exception(
953
                value,
954
                traceback)))
955