Completed
Push — master ( 6c104f...735829 )
by De
01:04
created

suggest_bad_operand_for_unary()   A

Complexity

Conditions 2

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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