suggest_attribute_alternative()   F
last analyzed

Complexity

Conditions 25

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 44
rs 2.6361
c 1
b 0
f 0
cc 25

How to fix   Complexity   

Complexity

Complex classes like suggest_attribute_alternative() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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