Completed
Push — master ( 84ff32...ae1382 )
by De
01:12
created

example_of_suggest_function()   A

Complexity

Conditions 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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