Completed
Push — master ( ba5da2...8ab8de )
by De
01:20
created

suggest_max_resursion_depth()   A

Complexity

Conditions 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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