Completed
Push — master ( 88603b...3e28ae )
by De
01:20
created

suggest_obj_has_no()   A

Complexity

Conditions 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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