Completed
Push — master ( fa297f...267ed8 )
by De
56s
created

suggest_attribute_as_special_case()   A

Complexity

Conditions 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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