Completed
Push — master ( c2ded7...1dc294 )
by De
54s
created

suggest_unsupported_op()   A

Complexity

Conditions 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
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
    elif attribute == '__or__':
430
        if '__pow__' in attributes:
431
            yield quote('val1 ** val2')
432
433
434
def suggest_attribute_synonyms(attribute, attributes):
435
    """Suggest that a method with a similar meaning was used.
436
437
    Example: 'lst.add(e)' -> 'lst.append(e)'.
438
    """
439
    for set_sub in SYNONYMS_SETS:
440
        if attribute in set_sub:
441
            for syn in set_sub & attributes:
442
                yield quote(syn)
443
444
445
def suggest_attribute_as_typo(attribute, attributes):
446
    """Suggest the attribute could be a typo.
447
448
    Example: 'a.do_baf()' -> 'a.do_bar()'.
449
    """
450
    for name in get_close_matches(attribute, attributes):
451
        # Handle Private name mangling
452
        if name.startswith('_') and '__' in name and not name.endswith('__'):
453
            yield quote(name) + ' (but it is supposed to be private)'
454
        else:
455
            yield quote(name)
456
457
458
def suggest_attribute_as_special_case(attribute):
459
    """Suggest that attribute could be handled in a specific way."""
460
    special_cases = {
461
        'exc_type': EXC_ATTR_REMOVED_MSG,
462
        'exc_value': EXC_ATTR_REMOVED_MSG,
463
        'exc_traceback': EXC_ATTR_REMOVED_MSG,
464
    }
465
    result = special_cases.get(attribute)
466
    if result is not None:
467
        yield result
468
469
470
# Functions related to ImportError
471
@register_suggestion_for(ImportError, re.NOMODULE_RE)
472
def suggest_no_module(value, frame, groups):
473
    """Get the suggestions closest to the failing module import.
474
475
    Example: 'import maths' -> 'import math'.
476
    """
477
    del value, frame  # unused param
478
    module_str, = groups
479
    for name in get_close_matches(module_str, STAND_MODULES):
480
        yield quote(name)
481
482
483
@register_suggestion_for(ImportError, re.CANNOTIMPORT_RE)
484
def suggest_cannot_import(value, frame, groups):
485
    """Get the suggestions closest to the failing import."""
486
    del value  # unused param
487
    imported_name, = groups
488
    module_name = frame.f_code.co_names[0]
489
    return itertools.chain(
490
        suggest_imported_name_as_typo(imported_name, module_name, frame),
491
        suggest_import_from_module(imported_name, frame))
492
493
494
def suggest_imported_name_as_typo(imported_name, module_name, frame):
495
    """Suggest that imported name could be a typo from actual name in module.
496
497
    Example: 'from math import pie' -> 'from math import pi'.
498
    """
499
    dir_mod = dir(import_from_frame(module_name, frame))
500
    for name in get_close_matches(imported_name, dir_mod):
501
        yield quote(name)
502
503
504
def suggest_import_from_module(imported_name, frame):
505
    """Suggest than name could be found in a standard module.
506
507
    Example: 'from itertools import pi' -> 'from math import pi'.
508
    """
509
    for mod in STAND_MODULES:
510
        if imported_name in dir(import_from_frame(mod, frame)):
511
            yield quote('from {0} import {1}'.format(mod, imported_name))
512
513
514
# Functions related to TypeError
515
def suggest_feature_not_supported(attr, type_str, frame):
516
    """Get suggestion for unsupported feature."""
517
    # 'Object does not support <feature>' exceptions
518
    # can be somehow seen as attribute errors for magic
519
    # methods except for the fact that we do not want to
520
    # have any fuzzy logic on the magic method name.
521
    # Also, we want to suggest the implementation of the
522
    # missing method (it is it not on a builtin object).
523
    types = get_types_for_str(type_str, frame)
524
    attributes = set(a for t in types for a in dir(t))
525
    for s in suggest_attribute_alternative(attr, type_str, attributes):
526
        yield s
527
    if type_str not in frame.f_builtins and \
528
            type_str not in ('function', 'generator'):
529
        yield 'implement "' + attr + '" on ' + type_str
530
531
532
@register_suggestion_for(TypeError, re.UNSUBSCRIPTABLE_RE)
533
def suggest_unsubscriptable(value, frame, groups):
534
    """Get suggestions in case of UNSUBSCRIPTABLE error."""
535
    del value  # unused param
536
    type_str, = groups
537
    return suggest_feature_not_supported('__getitem__', type_str, frame)
538
539
540
@register_suggestion_for(TypeError, re.NOT_CALLABLE_RE)
541
def suggest_not_callable(value, frame, groups):
542
    """Get suggestions in case of NOT_CALLABLE error."""
543
    del value  # unused param
544
    type_str, = groups
545
    return suggest_feature_not_supported('__call__', type_str, frame)
546
547
548
@register_suggestion_for(TypeError, re.OBJ_DOES_NOT_SUPPORT_RE)
549
def suggest_obj_does_not_support(value, frame, groups):
550
    """Get suggestions in case of OBJ DOES NOT SUPPORT error."""
551
    del value  # unused param
552
    type_str, feature = groups
553
    FEATURES = {
554
        'indexing': '__getitem__',
555
        'item assignment': '__setitem__',
556
        'item deletion': '__delitem__',
557
    }
558
    attr = FEATURES.get(feature)
559
    if attr is None:
560
        return []
561
    return suggest_feature_not_supported(attr, type_str, frame)
562
563
564
@register_suggestion_for(TypeError, re.OBJECT_HAS_NO_FUNC_RE)
565
def suggest_obj_has_no(value, frame, groups):
566
    """Get suggestions in case of OBJECT_HAS_NO_FUNC."""
567
    del value  # unused param
568
    type_str, feature = groups
569
    if feature in ('length', 'len'):
570
        return suggest_feature_not_supported('__len__', type_str, frame)
571
    return []
572
573
574
@register_suggestion_for(TypeError, re.BAD_OPERAND_UNARY_RE)
575
def suggest_bad_operand_for_unary(value, frame, groups):
576
    """Get suggestions for BAD_OPERAND_UNARY."""
577
    del value  # unused param
578
    unary, type_str = groups
579
    UNARY_OPS = {
580
        '+': '__pos__',
581
        'pos': '__pos__',
582
        '-': '__neg__',
583
        'neg': '__neg__',
584
        '~': '__invert__',
585
        'abs()': '__abs__',
586
        'abs': '__abs__',
587
    }
588
    attr = UNARY_OPS.get(unary)
589
    if attr is None:
590
        return []
591
    return suggest_feature_not_supported(attr, type_str, frame)
592
593
594
@register_suggestion_for(TypeError, re.UNSUPPORTED_OP_RE)
595
def suggest_unsupported_op(value, frame, groups):
596
    """Get suggestions for UNSUPPORTED_OP_RE."""
597
    del value  # unused param
598
    binary, type1, type2 = groups
599
    BINARY_OPS = {
600
        '^': '__or__',
601
    }
602
    attr = BINARY_OPS.get(binary)
603
    if attr is None:
604
        return []
605
    # Suggestion is based on first type which may not be the best
606
    return suggest_feature_not_supported(attr, type1, frame)
607
608
609
def get_func_by_name(func_name, frame):
610
    """Get the function with the given name in the frame."""
611
    objs = get_objects_in_frame(frame)
612
    # Trying to fetch reachable objects: getting objects and attributes
613
    # for objects. We would go deeper (with a fixed point algorithm) but
614
    # it doesn't seem to be worth it. In any case, we'll be missing a few
615
    # possible functions.
616
    objects = [o.obj for lst in objs.values() for o in lst]
617
    for obj in list(objects):
618
        for a in dir(obj):
619
            attr = getattr(obj, a, None)
620
            if attr is not None:
621
                objects.append(attr)
622
    # Then, we filter for function with the correct name (the name being the
623
    # name on the function object which is not always the same from the
624
    # namespace).
625
    return [func
626
            for func in objects
627
            if getattr(func, '__name__', None) == func_name]
628
629
630
@register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG_RE)
631
def suggest_unexpected_keywordarg(value, frame, groups):
632
    """Get suggestions in case of UNEXPECTED_KEYWORDARG error."""
633
    del value  # unused param
634
    func_name, kw_arg = groups
635
    arg_names = set()
636
    functions = get_func_by_name(func_name, frame)
637
    func_codes = [f.__code__ for f in functions if hasattr(f, '__code__')]
638
    args = set([var for func in func_codes for var in func.co_varnames])
639
    for arg_name in get_close_matches(kw_arg, args):
640
        yield quote(arg_name)
641
    if kw_arg == 'cmp' and 'key' in args:
642
        yield CMP_ARG_REMOVED_MSG
643
644
645
@register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG2_RE)
646
def suggest_unexpected_keywordarg2(value, frame, groups):
647
    """Get suggestions in case of UNEXPECTED_KEYWORDARG2 error."""
648
    del value, frame  # unused param
649
    kw_arg, = groups
650
    if kw_arg == 'cmp':
651
        yield CMP_ARG_REMOVED_MSG
652
653
654
@register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG3_RE)
655
def suggest_unexpected_keywordarg3(value, frame, groups):
656
    """Get suggestions in case of UNEXPECTED_KEYWORDARG2 error."""
657
    del value, frame  # unused param
658
    func_name, = groups
659
    del func_name  # unused value
660
    return []  # no implementation so far
661
662
663
@register_suggestion_for(TypeError, re.NB_ARG_RE)
664
def suggest_nb_arg(value, frame, groups):
665
    """Get suggestions in case of NB ARGUMENT error."""
666
    del value  # unused param
667
    func_name, expected, given = groups
668
    expect_nb = 0 if expected == 'no' else int(expected)
669
    given_nb = int(given)
670
    objs = get_objects_in_frame(frame)
671
    del expect_nb, given_nb, objs, func_name  # for later
672
    return []
673
674
675
# Functions related to ValueError
676
@register_suggestion_for(ValueError, re.ZERO_LEN_FIELD_RE)
677
def suggest_zero_len_field(value, frame, groups):
678
    """Get suggestions in case of ZERO_LEN_FIELD."""
679
    del value, frame, groups  # unused param
680
    yield '{0}'
681
682
683
@register_suggestion_for(ValueError, re.TIME_DATA_DOES_NOT_MATCH_FORMAT_RE)
684
def suggest_time_data_is_wrong(value, frame, groups):
685
    """Get suggestions in case of TIME_DATA_DOES_NOT_MATCH_FORMAT_RE."""
686
    del value, frame  # unused param
687
    timedata, timeformat = groups
688
    if timedata.count('%') > timeformat.count('%%'):
689
        yield "to swap value and format parameters"
690
691
692
# Functions related to SyntaxError
693
@register_suggestion_for(SyntaxError, re.OUTSIDE_FUNCTION_RE)
694
def suggest_outside_func_error(value, frame, groups):
695
    """Get suggestions in case of OUTSIDE_FUNCTION error."""
696
    del value, frame  # unused param
697
    yield "to indent it"
698
    word, = groups
699
    if word == 'return':
700
        yield "'sys.exit([arg])'"
701
702
703
@register_suggestion_for(SyntaxError, re.FUTURE_FEATURE_NOT_DEF_RE)
704
def suggest_future_feature(value, frame, groups):
705
    """Get suggestions in case of FUTURE_FEATURE_NOT_DEF error."""
706
    del value  # unused param
707
    feature, = groups
708
    return suggest_imported_name_as_typo(feature, '__future__', frame)
709
710
711
@register_suggestion_for(SyntaxError, re.INVALID_COMP_RE)
712
def suggest_invalid_comp(value, frame, groups):
713
    """Get suggestions in case of INVALID_COMP error."""
714
    del value, frame, groups  # unused param
715
    yield quote('!=')
716
717
718
@register_suggestion_for(SyntaxError, re.NO_BINDING_NONLOCAL_RE)
719
def suggest_no_binding_for_nonlocal(value, frame, groups):
720
    """Get suggestions in case of NO BINDING FOR NONLOCAL."""
721
    del value  # unused param
722
    name, = groups
723
    objs = get_objects_in_frame(frame).get(name, [])
724
    for obj, scope in objs:
725
        if scope == 'global':
726
            # TODO_ENCLOSING: suggest close matches for enclosing
727
            yield quote('global ' + name)
728
729
730
@register_suggestion_for(SyntaxError, re.INVALID_SYNTAX_RE)
731
def suggest_invalid_syntax(value, frame, groups):
732
    """Get suggestions in case of INVALID_SYNTAX error."""
733
    del frame, groups  # unused param
734
    alternatives = {
735
        '<>': '!=',
736
        '&&': 'and',
737
        '||': 'or',
738
    }
739
    offset = value.offset
740
    if value.offset is not None:
741
        for shift in (0, 1):
742
            offset = value.offset + shift
743
            two_last = value.text[offset - 2:offset]
744
            alt = alternatives.get(two_last)
745
            if alt is not None:
746
                yield quote(alt)
747
                break
748
749
750
# Functions related to MemoryError
751
@register_suggestion_for(MemoryError, None)
752
def get_memory_error_sugg(value, frame, groups):
753
    """Get suggestions for MemoryError exception."""
754
    del value, groups  # unused param
755
    objs = get_objects_in_frame(frame)
756
    return itertools.chain.from_iterable(
757
        suggest_memory_friendly_equi(name, objs)
758
        for name in frame.f_code.co_names)
759
760
761
# Functions related to OverflowError
762
@register_suggestion_for(OverflowError, re.RESULT_TOO_MANY_ITEMS_RE)
763
def suggest_too_many_items(value, frame, groups):
764
    """Suggest for TOO_MANY_ITEMS error."""
765
    del value  # unused param
766
    func, = groups
767
    objs = get_objects_in_frame(frame)
768
    return suggest_memory_friendly_equi(func, objs)
769
770
771
def suggest_memory_friendly_equi(name, objs):
772
    """Suggest name of a memory friendly equivalent for a function."""
773
    suggs = {'range': ['xrange']}
774
    return [quote(s) for s in suggs.get(name, []) if s in objs]
775
776
777
# Functions related to RuntimeError
778
@register_suggestion_for(RuntimeError, re.MAX_RECURSION_DEPTH_RE)
779
def suggest_max_resursion_depth(value, frame, groups):
780
    """Suggest for MAX_RECURSION_DEPTH error."""
781
    # this is the real solution, make it the first suggestion
782
    del value, frame, groups  # unused param
783
    yield AVOID_REC_MSG
784
    yield "increase the limit with " \
785
          "`sys.setrecursionlimit(limit)` (current value" \
786
          " is %d)" % sys.getrecursionlimit()
787
788
789
# Functions related to IOError/OSError
790
@register_suggestion_for((IOError, OSError), None)
791
def get_io_os_error_sugg(value, frame, groups):
792
    """Get suggestions for IOError/OSError exception."""
793
    # https://www.python.org/dev/peps/pep-3151/
794
    del frame, groups  # unused param
795
    err, _ = value.args
796
    errnos = {
797
        errno.ENOENT: suggest_if_file_does_not_exist,
798
        errno.ENOTDIR: suggest_if_file_is_not_dir,
799
        errno.EISDIR: suggest_if_file_is_dir,
800
    }
801
    return errnos.get(err, lambda x: [])(value)
802
803
804
def suggest_if_file_does_not_exist(value):
805
    """Get suggestions when a file does not exist."""
806
    # TODO: Add fuzzy match
807
    filename = value.filename
808
    for func, name in (
809
            (os.path.expanduser, 'os.path.expanduser'),
810
            (os.path.expandvars, 'os.path.expandvars')):
811
        expanded = func(filename)
812
        if os.path.exists(expanded) and filename != expanded:
813
            yield quote(expanded) + " (calling " + name + ")"
814
815
816
def suggest_if_file_is_not_dir(value):
817
    """Get suggestions when a file should have been a dir and is not."""
818
    filename = value.filename
819
    yield quote(os.path.dirname(filename)) + " (calling os.path.dirname)"
820
821
822
def suggest_if_file_is_dir(value):
823
    """Get suggestions when a file is a dir and should not."""
824
    filename = value.filename
825
    listdir = sorted(os.listdir(filename))
826
    if listdir:
827
        trunc_l = listdir[:MAX_NB_FILES]
828
        truncated = listdir != trunc_l
829
        filelist = [quote(f) for f in trunc_l] + (["etc"] if truncated else [])
830
        yield "any of the {0} files in directory ({1})".format(
831
            len(listdir), ", ".join(filelist))
832
    else:
833
        yield "to add content to {0} first".format(filename)
834
835
836
def get_suggestions_for_exception(value, traceback):
837
    """Get suggestions for an exception."""
838
    frame = get_last_frame(traceback)
839
    return itertools.chain.from_iterable(
840
            func(value, frame)
841
            for error_type, functions in SUGGESTION_FUNCTIONS.items()
842
            if isinstance(value, error_type)
843
            for func in functions)
844
845
846
def add_string_to_exception(value, string):
847
    """Add string to the exception parameter."""
848
    # The point is to have the string visible when the exception is printed
849
    # or converted to string - may it be via `str()`, `repr()` or when the
850
    # exception is uncaught and displayed (which seems to use `str()`).
851
    # In an ideal world, one just needs to update `args` but apparently it
852
    # is not enough for SyntaxError, IOError, etc where other
853
    # attributes (`msg`, `strerror`, `reason`, etc) are to be updated too
854
    # (for `str()`, not for `repr()`).
855
    # Also, elements in args might not be strings or args might me empty
856
    # so we add to the first string and add the element otherwise.
857
    assert type(value.args) == tuple
858
    if string:
859
        lst_args = list(value.args)
860
        for i, arg in enumerate(lst_args):
861
            if isinstance(arg, str):
862
                lst_args[i] = arg + string
863
                break
864
        else:
865
            # if no string arg, add the string anyway
866
            lst_args.append(string)
867
        value.args = tuple(lst_args)
868
        for attr in ['msg', 'strerror', 'reason']:
869
            attrval = getattr(value, attr, None)
870
            if attrval is not None:
871
                setattr(value, attr, attrval + string)
872
873
874
def get_last_frame(traceback):
875
    """Extract last frame from a traceback."""
876
    # In some rare case, the give traceback might be None
877
    if traceback is None:
878
        return None
879
    while traceback.tb_next:
880
        traceback = traceback.tb_next
881
    return traceback.tb_frame
882
883
884
def add_suggestions_to_exception(type_, value, traceback):
885
    """Add suggestion to an exception.
886
887
    Arguments are such as provided by sys.exc_info().
888
    """
889
    assert isinstance(value, type_)
890
    add_string_to_exception(
891
        value,
892
        get_suggestion_string(
893
            get_suggestions_for_exception(
894
                value,
895
                traceback)))
896