Completed
Push — master ( 9a0071...f44f4c )
by De
01:14
created

suggest_func_no_kw_arg()   A

Complexity

Conditions 3

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
cc 3
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
@register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG_RE)
654
def suggest_unexpected_keywordarg(value, frame, groups):
655
    """Get suggestions in case of UNEXPECTED_KEYWORDARG error."""
656
    del value  # unused param
657
    func_name, kw_arg = groups
658
    functions = get_func_by_name(func_name, frame)
659
    func_codes = [f.__code__ for f in functions if hasattr(f, '__code__')]
660
    args = set([var for func in func_codes for var in func.co_varnames])
661
    for arg_name in get_close_matches(kw_arg, args):
662
        yield quote(arg_name)
663
    if kw_arg == 'cmp' and 'key' in args:
664
        yield CMP_ARG_REMOVED_MSG
665
666
667
@register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG2_RE)
668
def suggest_unexpected_keywordarg2(value, frame, groups):
669
    """Get suggestions in case of UNEXPECTED_KEYWORDARG2 error."""
670
    del value, frame  # unused param
671
    kw_arg, = groups
672
    if kw_arg == 'cmp':
673
        yield CMP_ARG_REMOVED_MSG
674
675
676
@register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG3_RE)
677
def suggest_unexpected_keywordarg3(value, frame, groups):
678
    """Get suggestions in case of UNEXPECTED_KEYWORDARG2 error."""
679
    del value, frame  # unused param
680
    func_name, = groups
681
    del func_name  # unused value
682
    return []  # no implementation so far
683
684
685
@register_suggestion_for(TypeError, re.NB_ARG_RE)
686
def suggest_nb_arg(value, frame, groups):
687
    """Get suggestions in case of NB ARGUMENT error."""
688
    del value  # unused param
689
    func_name, expected, given = groups
690
    expect_nb = 0 if expected == 'no' else int(expected)
691
    given_nb = int(given)
692
    objs = get_objects_in_frame(frame)
693
    del expect_nb, given_nb, objs, func_name  # for later
694
    return []
695
696
697
@register_suggestion_for(TypeError, re.FUNC_TAKES_NO_KEYWORDARG_RE)
698
def suggest_func_no_kw_arg(value, frame, groups):
699
    """Get suggestions for FUNC_TAKES_NO_KEYWORDARG_RE."""
700
    # C-Level functions don't have actual names for their arguments.
701
    # Therefore, trying to use them with keyword arguments leads to
702
    # errors but using them with positional arguments just work fine.
703
    # This behavior definitly deserves some suggestion.
704
    # More reading:
705
    # http://stackoverflow.com/questions/24463202/typeerror-get-takes-no-keyword-arguments
706
    # https://www.python.org/dev/peps/pep-0457/
707
    # https://www.python.org/dev/peps/pep-0436/#functions-with-positional-only-parameters
708
    # Note: a proper implementation of this function would:
709
    #  - retrieve the function object using the function name
710
    #  - check that the function does accept arguments but does not
711
    # accept keyword arguments before yielding the suggestion.
712
    # Unfortunately, introspection of builtin function is not possible as per
713
    # http://bugs.python.org/issue1748064 . Thus, the only thing we can look
714
    # for is if a function has no __code__ attribute.
715
    func_name, = groups
716
    functions = get_func_by_name(func_name, frame)
717
    if any([not hasattr(f, '__code__') for f in functions]):
718
        yield NO_KEYWORD_ARG_MSG
719
720
721
# Functions related to ValueError
722
@register_suggestion_for(ValueError, re.ZERO_LEN_FIELD_RE)
723
def suggest_zero_len_field(value, frame, groups):
724
    """Get suggestions in case of ZERO_LEN_FIELD."""
725
    del value, frame, groups  # unused param
726
    yield '{0}'
727
728
729
@register_suggestion_for(ValueError, re.TIME_DATA_DOES_NOT_MATCH_FORMAT_RE)
730
def suggest_time_data_is_wrong(value, frame, groups):
731
    """Get suggestions in case of TIME_DATA_DOES_NOT_MATCH_FORMAT_RE."""
732
    del value, frame  # unused param
733
    timedata, timeformat = groups
734
    if timedata.count('%') > timeformat.count('%%'):
735
        yield "to swap value and format parameters"
736
737
738
# Functions related to SyntaxError
739
@register_suggestion_for(SyntaxError, re.OUTSIDE_FUNCTION_RE)
740
def suggest_outside_func_error(value, frame, groups):
741
    """Get suggestions in case of OUTSIDE_FUNCTION error."""
742
    del value, frame  # unused param
743
    yield "to indent it"
744
    word, = groups
745
    if word == 'return':
746
        yield "'sys.exit([arg])'"
747
748
749
@register_suggestion_for(SyntaxError, re.FUTURE_FEATURE_NOT_DEF_RE)
750
def suggest_future_feature(value, frame, groups):
751
    """Get suggestions in case of FUTURE_FEATURE_NOT_DEF error."""
752
    del value  # unused param
753
    feature, = groups
754
    return suggest_imported_name_as_typo(feature, '__future__', frame)
755
756
757
@register_suggestion_for(SyntaxError, re.INVALID_COMP_RE)
758
def suggest_invalid_comp(value, frame, groups):
759
    """Get suggestions in case of INVALID_COMP error."""
760
    del value, frame, groups  # unused param
761
    yield quote('!=')
762
763
764
@register_suggestion_for(SyntaxError, re.NO_BINDING_NONLOCAL_RE)
765
def suggest_no_binding_for_nonlocal(value, frame, groups):
766
    """Get suggestions in case of NO BINDING FOR NONLOCAL."""
767
    del value  # unused param
768
    name, = groups
769
    objs = get_objects_in_frame(frame).get(name, [])
770
    for _, scope in objs:
771
        if scope == 'global':
772
            # TODO_ENCLOSING: suggest close matches for enclosing
773
            yield quote('global ' + name)
774
775
776
@register_suggestion_for(SyntaxError, re.INVALID_SYNTAX_RE)
777
def suggest_invalid_syntax(value, frame, groups):
778
    """Get suggestions in case of INVALID_SYNTAX error."""
779
    del frame, groups  # unused param
780
    alternatives = {
781
        '<>': '!=',
782
        '&&': 'and',
783
        '||': 'or',
784
    }
785
    offset = value.offset
786
    if value.offset is not None:
787
        for shift in (0, 1):
788
            offset = value.offset + shift
789
            two_last = value.text[offset - 2:offset]
790
            alt = alternatives.get(two_last)
791
            if alt is not None:
792
                yield quote(alt)
793
                break
794
795
796
# Functions related to MemoryError
797
@register_suggestion_for(MemoryError, None)
798
def get_memory_error_sugg(value, frame, groups):
799
    """Get suggestions for MemoryError exception."""
800
    del value, groups  # unused param
801
    objs = get_objects_in_frame(frame)
802
    return itertools.chain.from_iterable(
803
        suggest_memory_friendly_equi(name, objs)
804
        for name in frame.f_code.co_names)
805
806
807
# Functions related to OverflowError
808
@register_suggestion_for(OverflowError, re.RESULT_TOO_MANY_ITEMS_RE)
809
def suggest_too_many_items(value, frame, groups):
810
    """Suggest for TOO_MANY_ITEMS error."""
811
    del value  # unused param
812
    func, = groups
813
    objs = get_objects_in_frame(frame)
814
    return suggest_memory_friendly_equi(func, objs)
815
816
817
def suggest_memory_friendly_equi(name, objs):
818
    """Suggest name of a memory friendly equivalent for a function."""
819
    suggs = {'range': ['xrange']}
820
    return [quote(s) for s in suggs.get(name, []) if s in objs]
821
822
823
# Functions related to RuntimeError
824
@register_suggestion_for(RuntimeError, re.MAX_RECURSION_DEPTH_RE)
825
def suggest_max_resursion_depth(value, frame, groups):
826
    """Suggest for MAX_RECURSION_DEPTH error."""
827
    # this is the real solution, make it the first suggestion
828
    del value, frame, groups  # unused param
829
    yield AVOID_REC_MSG
830
    yield "increase the limit with " \
831
          "`sys.setrecursionlimit(limit)` (current value" \
832
          " is %d)" % sys.getrecursionlimit()
833
834
835
# Functions related to IOError/OSError
836
@register_suggestion_for((IOError, OSError), None)
837
def get_io_os_error_sugg(value, frame, groups):
838
    """Get suggestions for IOError/OSError exception."""
839
    # https://www.python.org/dev/peps/pep-3151/
840
    del frame, groups  # unused param
841
    err, _ = value.args
842
    errnos = {
843
        errno.ENOENT: suggest_if_file_does_not_exist,
844
        errno.ENOTDIR: suggest_if_file_is_not_dir,
845
        errno.EISDIR: suggest_if_file_is_dir,
846
    }
847
    return errnos.get(err, lambda x: [])(value)
848
849
850
def suggest_if_file_does_not_exist(value):
851
    """Get suggestions when a file does not exist."""
852
    # TODO: Add fuzzy match
853
    filename = value.filename
854
    for func, name in (
855
            (os.path.expanduser, 'os.path.expanduser'),
856
            (os.path.expandvars, 'os.path.expandvars')):
857
        expanded = func(filename)
858
        if os.path.exists(expanded) and filename != expanded:
859
            yield quote(expanded) + " (calling " + name + ")"
860
861
862
def suggest_if_file_is_not_dir(value):
863
    """Get suggestions when a file should have been a dir and is not."""
864
    filename = value.filename
865
    yield quote(os.path.dirname(filename)) + " (calling os.path.dirname)"
866
867
868
def suggest_if_file_is_dir(value):
869
    """Get suggestions when a file is a dir and should not."""
870
    filename = value.filename
871
    listdir = sorted(os.listdir(filename))
872
    if listdir:
873
        trunc_l = listdir[:MAX_NB_FILES]
874
        truncated = listdir != trunc_l
875
        filelist = [quote(f) for f in trunc_l] + (["etc"] if truncated else [])
876
        yield "any of the {0} files in directory ({1})".format(
877
            len(listdir), ", ".join(filelist))
878
    else:
879
        yield "to add content to {0} first".format(filename)
880
881
882
def get_suggestions_for_exception(value, traceback):
883
    """Get suggestions for an exception."""
884
    frame = get_last_frame(traceback)
885
    return itertools.chain.from_iterable(
886
            func(value, frame)
887
            for error_type, functions in SUGGESTION_FUNCTIONS.items()
888
            if isinstance(value, error_type)
889
            for func in functions)
890
891
892
def add_string_to_exception(value, string):
893
    """Add string to the exception parameter."""
894
    # The point is to have the string visible when the exception is printed
895
    # or converted to string - may it be via `str()`, `repr()` or when the
896
    # exception is uncaught and displayed (which seems to use `str()`).
897
    # In an ideal world, one just needs to update `args` but apparently it
898
    # is not enough for SyntaxError, IOError, etc where other
899
    # attributes (`msg`, `strerror`, `reason`, etc) are to be updated too
900
    # (for `str()`, not for `repr()`).
901
    # Also, elements in args might not be strings or args might me empty
902
    # so we add to the first string and add the element otherwise.
903
    assert type(value.args) == tuple
904
    if string:
905
        lst_args = list(value.args)
906
        for i, arg in enumerate(lst_args):
907
            if isinstance(arg, str):
908
                lst_args[i] = arg + string
909
                break
910
        else:
911
            # if no string arg, add the string anyway
912
            lst_args.append(string)
913
        value.args = tuple(lst_args)
914
        for attr in ['msg', 'strerror', 'reason']:
915
            attrval = getattr(value, attr, None)
916
            if attrval is not None:
917
                setattr(value, attr, attrval + string)
918
919
920
def get_last_frame(traceback):
921
    """Extract last frame from a traceback."""
922
    # In some rare case, the give traceback might be None
923
    if traceback is None:
924
        return None
925
    while traceback.tb_next:
926
        traceback = traceback.tb_next
927
    return traceback.tb_frame
928
929
930
def add_suggestions_to_exception(type_, value, traceback):
931
    """Add suggestion to an exception.
932
933
    Arguments are such as provided by sys.exc_info().
934
    """
935
    assert isinstance(value, type_)
936
    add_string_to_exception(
937
        value,
938
        get_suggestion_string(
939
            get_suggestions_for_exception(
940
                value,
941
                traceback)))
942