Completed
Push — master ( 154999...b1f2dd )
by De
52s
created

suggest_no_binding_for_nonlocal()   A

Complexity

Conditions 3

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