Completed
Push — master ( afe8eb...154999 )
by De
54s
created

get_subclasses()   A

Complexity

Conditions 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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