Completed
Push — master ( 887489...bcf9fa )
by De
56s
created

suggest_unexpected_keywordarg()   C

Complexity

Conditions 8

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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