Completed
Push — master ( 652866...ac3ef3 )
by De
01:15
created

partial_app()   A

Complexity

Conditions 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 2
1
# -*- coding: utf-8
2
"""Logic to add suggestions to exceptions."""
3
import keyword
4
import difflib
5
import didyoumean_re as re
6
import itertools
7
import inspect
8
import errno
9
import os
10
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
    return __import__(
161
        module_name,
162
        frame.f_globals,
163
        frame.f_locals)
164
165
166
# Functions related to NameError
167
def suggest_name_not_defined(value, frame, groups):
168
    """Get the suggestions for name in case of NameError."""
169
    del value  # unused param
170
    name, = groups
171
    objs = get_objects_in_frame(frame)
172
    return itertools.chain(
173
        suggest_name_as_attribute(name, objs),
174
        suggest_name_as_standard_module(name),
175
        suggest_name_as_name_typo(name, objs),
176
        suggest_name_as_keyword_typo(name),
177
        suggest_name_as_missing_import(name, objs, frame),
178
        suggest_name_as_special_case(name))
179
180
181
def suggest_name_as_attribute(name, objdict):
182
    """Suggest that name could be an attribute of an object.
183
184
    Example: 'do_stuff()' -> 'self.do_stuff()'.
185
    """
186
    for nameobj, objs in objdict.items():
187
        prev_scope = None
188
        for obj, scope in objs:
189
            if hasattr(obj, name):
190
                yield quote(nameobj + '.' + name) + \
191
                    ('' if prev_scope is None else
192
                     ' ({0} hidden by {1})'.format(scope, prev_scope))
193
                break
194
            prev_scope = scope
195
196
197
def suggest_name_as_missing_import(name, objdict, frame):
198
    """Suggest that name could come from missing import.
199
200
    Example: 'foo' -> 'import mod, mod.foo'.
201
    """
202
    for mod in STAND_MODULES:
203
        if mod not in objdict and name in dir(import_from_frame(mod, frame)):
204
            yield "'{0}' from {1} (not imported)".format(name, mod)
205
206
207
def suggest_name_as_standard_module(name):
208
    """Suggest that name could be a non-imported standard module.
209
210
    Example: 'os.whatever' -> 'import os' and then 'os.whatever'.
211
    """
212
    if name in STAND_MODULES:
213
        yield 'to import {0} first'.format(name)
214
215
216
def suggest_name_as_name_typo(name, objdict):
217
    """Suggest that name could be a typo (misspelled existing name).
218
219
    Example: 'foobaf' -> 'foobar'.
220
    """
221
    for name in get_close_matches(name, objdict.keys()):
222
        yield quote(name) + ' (' + objdict[name][0].scope + ')'
223
224
225
def suggest_name_as_keyword_typo(name):
226
    """Suggest that name could be a typo (misspelled keyword).
227
228
    Example: 'yieldd' -> 'yield'.
229
    """
230
    for name in get_close_matches(name, keyword.kwlist):
231
        yield quote(name) + " (keyword)"
232
233
234
def suggest_name_as_special_case(name):
235
    """Suggest that name could correspond to a typo with special handling."""
236
    special_cases = {
237
        # Imaginary unit is '1j' in Python
238
        'i': quote('1j') + " (imaginary unit)",
239
        'j': quote('1j') + " (imaginary unit)",
240
        # Shell commands entered in interpreter
241
        'pwd': quote('os.getcwd()'),
242
        'ls': quote('os.listdir(os.getcwd())'),
243
        'cd': quote('os.chdir(path)'),
244
        'rm': "'os.remove(filename)', 'shutil.rmtree(dir)' for recursive",
245
    }
246
    result = special_cases.get(name)
247
    if result is not None:
248
        yield result
249
250
251
# Functions related to AttributeError
252
def suggest_attribute_error(value, frame, groups):
253
    """Get suggestions in case of ATTRIBUTEERROR."""
254
    del value  # unused param
255
    type_str, attr = groups
256
    return get_attribute_suggestions(type_str, attr, frame)
257
258
259
def suggest_module_has_no_attr(value, frame, groups):
260
    """Get suggestions in case of MODULEHASNOATTRIBUTE."""
261
    del value  # unused param
262
    _, attr = groups  # name ignored for the time being
263
    return get_attribute_suggestions('module', attr, frame)
264
265
266
def get_attribute_suggestions(type_str, attribute, frame):
267
    """Get the suggestions closest to the attribute name for a given type."""
268
    types = get_types_for_str(type_str, frame)
269
    attributes = set(a for t in types for a in dir(t))
270
    if type_str == 'module':
271
        # For module, we manage to get the corresponding 'module' type
272
        # but the type doesn't bring much information about its content.
273
        # A hacky way to do so is to assume that the exception was something
274
        # like 'module_name.attribute' so that we can actually find the module
275
        # based on the name. Eventually, we check that the found object is a
276
        # module indeed. This is not failproof but it brings a whole lot of
277
        # interesting suggestions and the (minimal) risk is to have invalid
278
        # suggestions.
279
        module_name = frame.f_code.co_names[0]
280
        objs = get_objects_in_frame(frame)
281
        mod = objs[module_name][0].obj
282
        attributes = set(dir(mod))
283
284
    return itertools.chain(
285
        suggest_attribute_as_builtin(attribute, type_str, frame),
286
        suggest_attribute_as_removed(attribute, type_str, attributes),
287
        suggest_attribute_synonyms(attribute, attributes),
288
        suggest_attribute_as_typo(attribute, attributes))
289
290
291
def suggest_attribute_as_builtin(attribute, type_str, frame):
292
    """Suggest that a builtin was used as an attribute.
293
294
    Example: 'lst.len()' -> 'len(lst)'.
295
    """
296
    if attribute in frame.f_builtins:
297
        yield quote(attribute + '(' + type_str + ')')
298
299
300
def suggest_attribute_as_removed(attribute, type_str, attributes):
301
    """Suggest that attribute is removed (and give an alternative)."""
302
    if attribute == 'has_key' and '__contains__' in attributes:
303
        yield quote('key in ' + type_str)
304
305
306
def suggest_attribute_synonyms(attribute, attributes):
307
    """Suggest that a method with a similar meaning was used.
308
309
    Example: 'lst.add(e)' -> 'lst.append(e)'.
310
    """
311
    for set_sub in SYNONYMS_SETS:
312
        if attribute in set_sub:
313
            for syn in set_sub & attributes:
314
                yield quote(syn)
315
316
317
def suggest_attribute_as_typo(attribute, attributes):
318
    """Suggest the attribute could be a typo.
319
320
    Example: 'a.do_baf()' -> 'a.do_bar()'.
321
    """
322
    for name in get_close_matches(attribute, attributes):
323
        # Handle Private name mangling
324
        if name.startswith('_') and '__' in name and not name.endswith('__'):
325
            yield quote(name) + ' (but it is supposed to be private)'
326
        else:
327
            yield quote(name)
328
329
330
# Functions related to ImportError
331
def suggest_no_module(value, frame, groups):
332
    """Get the suggestions closest to the failing module import.
333
334
    Example: 'import maths' -> 'import math'.
335
    """
336
    del value, frame  # unused param
337
    module_str, = groups
338
    for name in get_close_matches(module_str, STAND_MODULES):
339
        yield quote(name)
340
341
342
def suggest_cannot_import(value, frame, groups):
343
    """Get the suggestions closest to the failing import."""
344
    del value  # unused param
345
    imported_name, = groups
346
    module_name = frame.f_code.co_names[0]
347
    return itertools.chain(
348
        suggest_imported_name_as_typo(imported_name, module_name, frame),
349
        suggest_import_from_module(imported_name, frame))
350
351
352
def suggest_imported_name_as_typo(imported_name, module_name, frame):
353
    """Suggest that imported name could be a typo from actual name in module.
354
355
    Example: 'from math import pie' -> 'from math import pi'.
356
    """
357
    dir_mod = dir(import_from_frame(module_name, frame))
358
    for name in get_close_matches(imported_name, dir_mod):
359
        yield quote(name)
360
361
362
def suggest_import_from_module(imported_name, frame):
363
    """Suggest than name could be found in a standard module.
364
365
    Example: 'from itertools import pi' -> 'from math import pi'.
366
    """
367
    for mod in STAND_MODULES:
368
        if imported_name in dir(import_from_frame(mod, frame)):
369
            yield quote('from {0} import {1}'.format(mod, imported_name))
370
371
372
# Functions related to TypeError
373
def suggest_unsubscriptable(value, frame, groups):
374
    """Get suggestions in case of UNSUBSCRIBTABLE error."""
375
    del value  # unused param
376
    type_str, = groups
377
    types = get_types_for_str(type_str, frame)
378
    if any(hasattr(t, '__call__') for t in types):
379
        yield quote(type_str + '(value)')
380
381
382
def suggest_unexpected_keywordarg(value, frame, groups):
383
    """Get suggestions in case of UNEXPECTED_KEYWORDARG error."""
384
    del value  # unused param
385
    func_name, kw_arg = groups
386
    objs = get_objects_in_frame(frame)
387
    if func_name in objs:
388
        func = objs[func_name][0].obj
389
        args = func.__code__.co_varnames
390
        for name in get_close_matches(kw_arg, args):
391
            yield quote(name)
392
393
394
def suggest_unexpected_keywordarg2(value, frame, groups):
395
    """Get suggestions in case of UNEXPECTED_KEYWORDARG2 error."""
396
    del value
397
    del frame  # unused param
398
    del groups  # unused param
399
    return []  # no implementation so far
400
401
402
def suggest_not_callable(value, frame, groups):
403
    """Get suggestions in case of NOT_CALLABLE error."""
404
    del value  # unused param
405
    type_str, = groups
406
    types = get_types_for_str(type_str, frame)
407
    if any(hasattr(t, '__getitem__') for t in types):
408
        yield quote(type_str + '[value]')
409
410
411
# Functions related to ValueError
412
def suggest_zero_len_field(value, frame, groups):
413
    """Get suggestions in case of ZERO_LEN_FIELD."""
414
    del value, frame, groups  # unused param
415
    yield '{0}'
416
417
418
def suggest_time_data_is_wrong(value, frame, groups):
419
    """Get suggestions in case of TIME_DATA_DOES_NOT_MATCH_FORMAT_RE."""
420
    timedata, timeformat = groups
421
    if timedata.count('%') > timeformat.count('%%'):
422
        yield "to swap value and format parameters"
423
424
425
# Functions related to SyntaxError
426
def suggest_outside_func_error(value, frame, groups):
427
    """Get suggestions in case of OUTSIDE_FUNCTION error."""
428
    del value, frame  # unused param
429
    yield "to indent it"
430
    word, = groups
431
    if word == 'return':
432
        yield "'sys.exit([arg])'"
433
434
435
def suggest_future_feature(value, frame, groups):
436
    """Get suggestions in case of FUTURE_FEATURE_NOT_DEF error."""
437
    del value  # unused param
438
    feature, = groups
439
    return suggest_imported_name_as_typo(feature, '__future__', frame)
440
441
442
def suggest_invalid_comp(value, frame, groups):
443
    """Get suggestions in case of INVALID_COMP error."""
444
    del value, frame, groups  # unused param
445
    yield quote('!=')
446
447
448
def suggest_invalid_syntax(value, frame, groups):
449
    """Get suggestions in case of INVALID_SYNTAX error."""
450
    del frame, groups  # unused param
451
    offset = value.offset
452
    if offset is not None and offset > 2:
453
        two_last = value.text[offset - 2:offset]
454
        if two_last == '<>':
455
            yield quote('!=')
456
457
458
# Functions related to MemoryError
459
def get_memory_error_sugg(value, frame):
460
    """Get suggestions for MemoryError exception."""
461
    assert isinstance(value, MemoryError)
462
    objs = get_objects_in_frame(frame)
463
    return itertools.chain.from_iterable(
464
        suggest_memory_friendly_equi(name, objs)
465
        for name in frame.f_code.co_names)
466
467
468
# Functions related to OverflowError
469
def suggest_too_many_items(value, frame, groups):
470
    """Suggest for TOO_MANY_ITEMS error."""
471
    del value  # unused param
472
    func, = groups
473
    objs = get_objects_in_frame(frame)
474
    return suggest_memory_friendly_equi(func, objs)
475
476
477
def suggest_memory_friendly_equi(name, objs):
478
    """Suggest name of a memory friendly equivalent for a function."""
479
    suggs = {'range': ['xrange']}
480
    return [quote(s) for s in suggs.get(name, []) if s in objs]
481
482
483
# Functions related to IOError/OSError
484
def get_io_os_error_sugg(value, frame):
485
    """Get suggestions for IOError/OSError exception."""
486
    # https://www.python.org/dev/peps/pep-3151/
487
    del frame  # unused param
488
    assert isinstance(value, (IOError, OSError))
489
    err, _ = value.args
490
    errnos = {
491
        errno.ENOENT: suggest_if_file_does_not_exist,
492
        errno.ENOTDIR: suggest_if_file_is_not_dir,
493
        errno.EISDIR: suggest_if_file_is_dir,
494
    }
495
    return errnos.get(err, lambda x: [])(value)
496
497
498
def suggest_if_file_does_not_exist(value):
499
    """Get suggestions when a file does not exist."""
500
    # TODO: Add fuzzy match
501
    filename = value.filename
502
    for func, name in (
503
            (os.path.expanduser, 'os.path.expanduser'),
504
            (os.path.expandvars, 'os.path.expandvars')):
505
        expanded = func(filename)
506
        if os.path.exists(expanded) and filename != expanded:
507
            yield quote(expanded) + " (calling " + name + ")"
508
509
510
def suggest_if_file_is_not_dir(value):
511
    """Get suggestions when a file should have been a dir and is not."""
512
    filename = value.filename
513
    yield quote(os.path.dirname(filename)) + " (calling os.path.dirname)"
514
515
516
def suggest_if_file_is_dir(value):
517
    """Get suggestions when a file is a dir and should not."""
518
    filename = value.filename
519
    listdir = sorted(os.listdir(filename))
520
    if listdir:
521
        trunc_l = listdir[:MAX_NB_FILES]
522
        truncated = listdir != trunc_l
523
        filelist = [quote(f) for f in trunc_l] + (["etc"] if truncated else [])
524
        yield "any of the {0} files in directory ({1})".format(
525
            len(listdir), ", ".join(filelist))
526
    else:
527
        yield "to add content to {0} first".format(filename)
528
529
530
NAMEERRORS = {
531
    re.VARREFBEFOREASSIGN_RE: suggest_name_not_defined,
532
    re.NAMENOTDEFINED_RE: suggest_name_not_defined,
533
}
534
535
ATTRIBUTEERRORS = {
536
    re.ATTRIBUTEERROR_RE: suggest_attribute_error,
537
    re.MODULEHASNOATTRIBUTE_RE: suggest_module_has_no_attr,
538
}
539
540
TYPEERRORS = {
541
    re.UNSUBSCRIBTABLE_RE: suggest_unsubscriptable,
542
    re.UNEXPECTED_KEYWORDARG_RE: suggest_unexpected_keywordarg,
543
    re.UNEXPECTED_KEYWORDARG2_RE: suggest_unexpected_keywordarg2,
544
    re.NOT_CALLABLE_RE: suggest_not_callable,
545
}
546
547
VALUEERRORS = {
548
    re.ZERO_LEN_FIELD_RE: suggest_zero_len_field,
549
    re.TIME_DATA_DOES_NOT_MATCH_FORMAT_RE: suggest_time_data_is_wrong,
550
}
551
552
SYNTAXERRORS = {
553
    re.OUTSIDE_FUNCTION_RE: suggest_outside_func_error,
554
    re.FUTURE_FEATURE_NOT_DEF_RE: suggest_future_feature,
555
    re.INVALID_COMP_RE: suggest_invalid_comp,
556
    re.INVALID_SYNTAX_RE: suggest_invalid_syntax,
557
}
558
559
OVERFLOWERRORS = {
560
    re.RESULT_TOO_MANY_ITEMS_RE: suggest_too_many_items
561
}
562
563
IMPORTERRORS = {
564
    re.NOMODULE_RE: suggest_no_module,
565
    re.CANNOTIMPORT_RE: suggest_cannot_import,
566
}
567
568
569
def get_suggestions_for_error(value, frame, re_dict):
570
    """Get suggestions for a given error `value`.
571
572
    Call relevant suggestion generator based on error message
573
    and regex dictionnary.
574
    """
575
    error_msg = value.args[0]
576
    for regex, generator in re_dict.items():
577
        match = re.match(regex, error_msg)
578
        if match:
579
            return generator(value, frame, match.groups())
580
    return []
581
582
583
def get_suggestions_for_exception(value, traceback):
584
    """Get suggestions for an exception."""
585
    frame = get_last_frame(traceback)
586
587
    def partial_app(re_dict):
588
        """Partial application of get_suggestions_for_error to re_dict."""
589
        return (lambda val, fram:
590
                get_suggestions_for_error(val, fram, re_dict))
591
592
    error_types = {
593
        NameError: partial_app(NAMEERRORS),
594
        AttributeError: partial_app(ATTRIBUTEERRORS),
595
        TypeError: partial_app(TYPEERRORS),
596
        ValueError: partial_app(VALUEERRORS),
597
        ImportError: partial_app(IMPORTERRORS),
598
        SyntaxError: partial_app(SYNTAXERRORS),
599
        OverflowError: partial_app(OVERFLOWERRORS),
600
        MemoryError: get_memory_error_sugg,
601
        (IOError, OSError): get_io_os_error_sugg,
602
    }
603
    return itertools.chain.from_iterable(
604
        generator(value, frame)
605
        for error_type, generator in error_types.items()
606
        if isinstance(value, error_type))
607
608
609
def add_string_to_exception(value, string):
610
    """Add string to the exception parameter."""
611
    # The point is to have the string visible when the exception is printed
612
    # or converted to string - may it be via `str()`, `repr()` or when the
613
    # exception is uncaught and displayed (which seems to use `str()`).
614
    # In an ideal world, one just needs to update `args` but apparently it
615
    # is not enough for SyntaxError, IOError, etc where other
616
    # attributes (`msg`, `strerror`, `reason`, etc) are to be updated too
617
    # (for `str()`, not for `repr()`).
618
    # Also, elements in args might not be strings or args might me empty
619
    # so we add to the first string and add the element otherwise.
620
    assert type(value.args) == tuple
621
    if string:
622
        lst_args = list(value.args)
623
        for i, arg in enumerate(lst_args):
624
            if isinstance(arg, str):
625
                lst_args[i] = arg + string
626
                break
627
        else:
628
            # if no string arg, add the string anyway
629
            lst_args.append(string)
630
        value.args = tuple(lst_args)
631
        for attr in ['msg', 'strerror', 'reason']:
632
            attrval = getattr(value, attr, None)
633
            if attrval is not None:
634
                setattr(value, attr, attrval + string)
635
636
637
def get_last_frame(traceback):
638
    """Extract last frame from a traceback."""
639
    while traceback.tb_next:
640
        traceback = traceback.tb_next
641
    return traceback.tb_frame
642
643
644
def add_suggestions_to_exception(type_, value, traceback):
645
    """Add suggestion to an exception.
646
647
    Arguments are such as provided by sys.exc_info().
648
    """
649
    assert isinstance(value, type_)
650
    add_string_to_exception(
651
        value,
652
        get_suggestion_string(
653
            get_suggestions_for_exception(
654
                value,
655
                traceback)))
656