File: attribute_error.py

package info (click to toggle)
python-friendly-traceback 0.7.62%2Bgit20240811.d7dbff6-1.1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,264 kB
  • sloc: python: 21,500; makefile: 4
file content (536 lines) | stat: -rw-r--r-- 21,557 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
"""Getting specific information for AttributeError"""

import ast
import builtins
import platform
import re
import sys
from types import FrameType
from typing import Any, Iterable, List, Optional, Sequence

from .. import console_helpers, debug_helper, info_variables, utils
from ..ft_gettext import current_lang, please_report
from ..message_parser import get_parser
from ..path_info import path_utils
from ..tb_data import TracebackData  # for type checking only
from ..typing_info import CauseInfo  # for type checking only
from ..utils import get_similar_words, list_to_string
from . import stdlib_modules

parser = get_parser(AttributeError)
_ = current_lang.translate


@parser._add
def circular_import(message: str, _tb_data: TracebackData) -> CauseInfo:
    pattern = re.compile(r"partially initialized module '(.*)' has")
    match = re.search(pattern, message)
    if not match:
        return {}
    module = match[1]

    if stdlib_modules.module_exists(module):
        hint = _("Did you give your program the same name as a Python module?\n")
        cause = _(
            "I suspect that you used the name `{module}.py` for your program\n"
            "and that you also wanted to import a module with the same name\n"
            "from Python's standard library.\n"
            "If so, you should use a different name for your program.\n"
        ).format(module=module)
        return {"cause": cause, "suggest": hint}

    if "circular import" in message:
        hint = _("You have a circular import.\n")
    else:  # pragma: no cover
        # I thought a version did not include the mention of circular import.
        debug_helper.log("New case to consider for circular_import.")
        hint = _("You likely have a circular import.\n")
    cause = _("Python indicated that the module `{module}` was not fully imported.\n")
    cause += _(
        "This can occur if, during the execution of the code in module `{module}`\n"
        "an attempt is made to import the same module again.\n"
    ).format(module=module)
    return {"cause": cause, "suggest": hint}


# ======= Attribute error in module =========


@parser._add
def attribute_error_in_module(message: str, tb_data: TracebackData) -> CauseInfo:
    """Attempts to find if a module attribute or module name might have been misspelled"""
    pattern = re.compile(r"module '(.*)' has no attribute '(.*)'")
    match = re.search(pattern, message)
    if not match:
        return {}
    module = match[1]
    attribute = match[2]
    frame = tb_data.exception_frame

    try:
        mod = sys.modules[module]
    except Exception:  # noqa
        cause = _(
            "This should not happen:\n"
            "Python tells us that module `{module}` does not have an "
            "attribute named `{attribute}`.\n"
            "However, it does not appear that module `{module}` was imported.\n"
        ).format(module=module, attribute=attribute)
        if stdlib_modules.module_exists(module):
            hint = _("Did you give your program the same name as a Python module?\n")
            cause += _(
                "I suspect that you used the name `{module}.py` for your program\n"
                "and that you also wanted to import a module with the same name\n"
                "from Python's standard library.\n"
                "If so, you should use a different name for your program.\n"
            ).format(module=module)
            return {"cause": cause, "suggest": hint}
        else:
            cause = hint = _("You likely have a circular import.\n")
            cause += _(
                "This can occur if, during the execution of the code in module `{module}`\n"
                "an attempt is made to import the same module again.\n"
            ).format(module=module)
        return {"cause": cause, "suggest": hint}

    similar_attributes = get_similar_words(attribute, dir(mod))
    if similar_attributes:
        if len(similar_attributes) == 1:
            hint = _("Did you mean `{name}`?\n").format(name=similar_attributes[0])
            cause = _(
                "Perhaps you meant to write `{module}.{correct}` "
                "instead of `{module}.{typo}`\n"
            ).format(correct=similar_attributes[0], typo=attribute, module=module)
            return {"cause": cause, "suggest": hint}

        names = list_to_string(similar_attributes)
        hint = _("Did you mean `{name}`?\n").format(name=similar_attributes[0])
        cause = _(
            "Instead of writing `{module}.{typo}`, perhaps you meant to write one of \n"
            "the following names which are attributes of module `{module}`:\n"
            "`{names}`\n"
        ).format(names=names, typo=attribute, module=module)
        return {"cause": cause, "suggest": hint}

    if stdlib_modules.module_exists(module) and hasattr(mod, "__file__"):
        mod_path = path_utils.shorten_path(mod.__file__)
        if not mod_path.startswith("PYTHON_LIB:"):
            hint = _("Did you give your program the same name as a Python module?\n")
            cause = _(
                "You imported a module named `{module}` from `{mod_path}`.\n"
                "There is also a module named `{module}` in Python's standard library.\n"
                "Perhaps you need to rename your module.\n"
            ).format(module=module, mod_path=mod_path)
            return {"cause": cause, "suggest": hint}

    imported_modules = []
    for mod_name in sys.modules:
        if mod_name in frame.f_locals:
            imported_modules.append((mod_name, frame.f_locals[mod_name]))
        elif mod_name in frame.f_globals:
            imported_modules.append((mod_name, frame.f_globals[mod_name]))

    possible_modules = [
        mod_name for mod_name, mod in imported_modules if attribute in dir(mod)
    ]

    if possible_modules:
        if len(possible_modules) == 1:
            mod_name = possible_modules[0]
            hint = _("Did you mean `{name}`?\n").format(name=mod_name)
            cause = _(
                "Perhaps you meant to use the attribute `{attribute}` of \n"
                "module `{mod_name}` instead of module `{module}`.\n"
            ).format(attribute=attribute, mod_name=mod_name, module=module)
            return {"cause": cause, "suggest": hint}

        hint = _("Did you mean one of the following modules: `{names}`?").format(
            names=list_to_string(possible_modules)
        )
        cause = _(
            "Instead of the module `{module}`, perhaps you wanted to use\n"
            "the attribute `{attribute}` of one of the following modules:\n"
            "`{names}`.\n"
        ).format(
            attribute=attribute, module=module, names=list_to_string(possible_modules)
        )
        return {"cause": cause, "suggest": hint}

    cause = _(
        "Python tells us that no object with name `{attribute}` is\n"
        "found in module `{module}`.\n"
    ).format(attribute=attribute, module=module)
    return {"cause": cause}


@parser._add
def type_object_has_no_attribute(message: str, tb_data: TracebackData) -> CauseInfo:
    """Attempts to find if a module attribute or module name might have been misspelled"""
    pattern = re.compile(r"type object '(.*)' has no attribute '(.*)'")
    match = re.search(pattern, message)
    if not match:
        return {}
    frame = tb_data.exception_frame
    return _attribute_error_in_object(match[1], match[2], tb_data, frame)


def _invalid_add_note(exc_name, frame):
    exc = info_variables.get_object_from_name(exc_name, frame)
    if issubclass(exc, BaseException):
        hint = _("`add_note()` is only allowed for Python version 3.11 and newer.\n")
        using = _("You are using Python version {version}.\n").format(
            version=platform.python_version()
        )
        return {"cause": hint + using, "suggest": hint}
    return {}


@parser._add
def attribute_error_in_object(message: str, tb_data: TracebackData) -> CauseInfo:
    pattern = re.compile(r"'(.*)' object has no attribute '(.*)'")
    match = re.search(pattern, message)
    if not match:
        return {}
    frame = tb_data.exception_frame

    if match[1] == "NoneType":
        return {
            "cause": _(
                "You are attempting to access the attribute `{attr}`\n"
                "for a variable whose value is `None`."
            ).format(attr=match[2])
        }
    if match[2] == "add_note" and sys.version_info < (3, 11):
        cause = _invalid_add_note(match[1], frame)
        if cause:
            return cause

    return _attribute_error_in_object(match[1], match[2], tb_data, frame)


@parser._add
def object_attribute_is_read_only(message: str, tb_data: TracebackData) -> CauseInfo:
    pattern = r"'(.*)' object attribute '(.*)' is read-only"
    match = re.search(pattern, message)
    if match is None:
        return {}
    obj_type = match[1]
    attribute = match[2]
    frame = tb_data.exception_frame

    obj = info_variables.get_object_from_name(obj_type, frame)
    if not hasattr(obj, "__slots__"):
        cause = _(
            "The value of attribute `{attribute}` of objects of type `{obj_type}`\n"
            "cannot be changed.\n"
            "I have no further information.\n"
        ).format(attribute=attribute, obj_type=obj_type)
        return {"cause": cause}

    slots = getattr(obj, "__slots__")
    all_objects = info_variables.get_all_objects(tb_data.bad_line, frame)["name, obj"]
    for obj_name, instance in all_objects:
        try:
            if isinstance(instance, obj) or instance == obj:
                break
        except Exception:  # noqa
            continue
    else:
        obj_name = obj_type

    if len(slots) == 0:
        cause = _(
            "Object `{name}` uses an empty `__slots__` to prevent the modification\n"
            "of any of its attributes.\n"
        ).format(name=obj_name, attribute=attribute)
        return {"cause": cause}

    cause = _(
        "Object `{name}` uses `__slots__` to specify which attributes can\n"
        "be changed. The value of attribute `{name}.{attribute}` cannot be changed.\n"
    ).format(name=obj_name, attribute=attribute)
    if len(slots) == 1:
        cause += (
            "The only attribute of `{name}` whose value can be changed is"
            "`{attribute}`.\n"
        ).format(name=obj_name, attribute=slots[0])
    elif len(slots) < 10:
        cause += (
            "The only attributes of `{name}` whose value can be changed are:\n"
            "`{attributes}`.\n"
        ).format(name=obj_name, attributes=utils.list_to_string(slots))
    return {"cause": cause}


def _attribute_error_in_object(
    obj_type: str, attribute: str, tb_data: TracebackData, frame: FrameType
) -> CauseInfo:
    """Attempts to find if object attribute might have been misspelled"""
    if obj_type == "builtin_function_or_method":
        obj_name = tb_data.bad_line.replace("." + attribute, "")
        # Confirm we have the right one
        if obj_name in dir(builtins):
            cause = _(
                "`{obj_name}` is a function. Perhaps you meant to write\n"
                "`{obj_name}({attribute})`\n"
            ).format(obj_name=obj_name, attribute=attribute)
            hint = _("Did you mean `{obj_name}({attribute})`?\n").format(
                obj_name=obj_name, attribute=attribute
            )
            return {"cause": cause, "suggest": hint}

        cause = _(
            "`{obj_name}` is a Python built-in function or method\n"
            "which does not have an attribute named `{attribute}.`\n"
        ).format(obj_name=obj_name, attribute=attribute)
        return {"cause": cause}

    all_objects = info_variables.get_all_objects(tb_data.bad_line, frame)["name, obj"]
    obj = info_variables.get_object_from_name(obj_type, frame)

    if obj is None:  # object could be an instance of obj_type
        # this is to fix issue #212
        for obj_name, _obj in all_objects:
            t = str(type(_obj))
            if (
                t.endswith(f".{obj_type}'>") or t.endswith(f"'{obj_type}'>")
            ) and not hasattr(_obj, attribute):
                instance = _obj
                break
        else:
            cause = _(
                "An object of type `{obj_type}` has no attribute named `{attr}`.\n"
                "Unfortunately I cannot find such an object on the line where\n"
                "the problem occurs.\n"
            ).format(obj_type=obj_type, attr=attribute)
            return {"cause": cause + please_report()}
    else:
        for obj_name, instance in all_objects:
            try:
                if isinstance(instance, obj) or instance == obj:
                    break
            except TypeError:
                pass
        else:
            possible_objects = []
            for obj_name, _obj in all_objects:
                t = str(type(_obj))
                if (
                    (t.endswith(f".{obj_type}'>") or t.endswith(f"'{obj_type}'>"))
                    and not hasattr(_obj, attribute)
                ) or (
                    obj_name == "Friendly"
                    and isinstance(_obj, console_helpers.FriendlyHelpers)
                ):
                    possible_objects.append((obj_name, _obj))

            if not possible_objects:
                debug_helper.log(f"bad_line= {tb_data.bad_line}")
                cause = _(
                    "An object of type `{obj_type}` has no attribute named `{attr}`.\n\n"
                    "I cannot give additional information:\n"
                    "I found one object of this type in the current scope\n"
                    "but it does not appear to be the object causing the problem.\n"
                ).format(obj_type=obj_type, attr=attribute)
                return {"cause": cause + please_report()}
            elif len(possible_objects) > 1:
                names = ", ".join(name for name, _obj in possible_objects)
                cause = _(
                    "An object of type `{obj_type}` has no attribute named `{attr}`.\n\n"
                    "The following objects might be the cause of the problem: \n"
                    "{names}.\n"
                ).format(obj_type=obj_type, attr=attribute, names=names)
                return {"cause": cause}

            obj_name, instance = possible_objects[0]

    possible_cause = tuple_by_accident(instance, obj_name, attribute)
    if possible_cause:
        return possible_cause

    known_attributes = dir(instance)

    # Example: this.len -> len(this)
    known_builtin = perhaps_builtin(attribute, known_attributes)
    if known_builtin:
        return use_builtin_function(obj_name, attribute, known_builtin)

    if attribute == "join":
        join = perhaps_join(obj)
        if join:
            return use_str_join(obj_name, tb_data)

    # Example: both "this" and "that" are known objects
    # this.that -> this, that
    if attribute in frame.f_globals or attribute in frame.f_locals:
        return missing_comma(obj_name, attribute)

    known_synonyms = perhaps_synonym(attribute, known_attributes)
    if known_synonyms:
        return use_synonym(obj_name, attribute, known_synonyms)

    # noqa Example: list.apend -> list.append
    similar = get_similar_words(attribute, known_attributes)
    if similar:
        return handle_attribute_typo(obj_name, attribute, similar)

    # We have not been able to find a useful suggestion
    cause = _("The object `{obj}` has no attribute named `{attr}`.\n").format(
        obj=obj_name, attr=attribute
    )
    if hasattr(obj, "__slots__"):
        cause += _(
            "Note that object `{obj}` uses `__slots__` which prevents\n"
            "the creation of new attributes.\n"
        ).format(obj=obj_name)
    known_attributes = [a for a in known_attributes if "__" not in a]
    if len(known_attributes) > 10:
        known_attributes = known_attributes[:9] + ["..."]
    if known_attributes:
        cause += _(
            "The following are some of its known attributes:\n`{names}`."
        ).format(names=", ".join(known_attributes))
    return {"cause": cause}


def handle_attribute_typo(
    obj_name: str, attribute: str, similar: Sequence[str]
) -> CauseInfo:
    """Takes care of misspelling of existing attribute of object whose
    name could be identified.
    """
    cause = _("The object `{obj_name}` has no attribute named `{attribute}`.\n").format(
        obj_name=obj_name, attribute=attribute
    )
    if len(similar) == 1:
        hint = _("Did you mean `{name}`?\n").format(name=similar[0])
        cause += _(
            "Perhaps you meant to write `{obj}.{correct}` "
            "instead of `{obj}.{typo}`\n"
        ).format(correct=similar[0], typo=attribute, obj=obj_name)
    else:
        names = list_to_string(similar)
        hint = _("Did you mean one of the following: `{names}`?\n").format(names=names)
        cause += _(
            "Instead of writing `{obj}.{typo}`, perhaps you meant to write one of \n"
            "the following names which are attributes of object `{obj}`:\n"
            "`{names}`\n"
        ).format(names=names, typo=attribute, obj=obj_name)
    return {"cause": cause, "suggest": hint}


def perhaps_builtin(attribute: str, known_attributes: Iterable[str]) -> Optional[str]:
    if attribute in {"min", "max", "sorted", "reversed", "sum"}:
        return attribute
    elif (
        attribute in {"len", "length", "lenght", "size"}  # noqa
        and "__len__" in known_attributes
    ):
        return "len"
    return None


def tuple_by_accident(obj: Any, obj_name: str, attribute: str) -> CauseInfo:
    if not (isinstance(obj, tuple) and len(obj) == 1):
        return {}

    true_obj = obj[0]
    if hasattr(true_obj, attribute):
        hint = _("Did you write a comma by mistake?\n")
        cause = _(
            "`{obj_name}` is a tuple that contains a single item\n"
            "which does have `'{attribute}'` as an attribute.\n"
            "Perhaps you added a trailing comma by mistake at the end of the line\n"
            "where you defined `{obj_name}`.\n"
        ).format(obj_name=obj_name, attribute=attribute)
        return {"cause": cause, "suggest": hint}

    return {}


def use_str_join(obj_name: str, tb_data: TracebackData) -> CauseInfo:
    str_content = repr("...")
    hint = _("Did you mean `{str_content}.join({obj_name})`?\n")
    cause = _(
        "The object `{obj_name}` has no attribute named `join`.\n"
        "Perhaps you wanted something like `{str_content}.join({obj_name})`.\n"
    )
    parts = tb_data.original_bad_line.split(tb_data.bad_line)
    if len(parts) >= 2 and parts[1].strip().startswith("("):
        index = parts[1].find(")")
        if index != -1:
            content = parts[1][1:index].strip()
            try:
                content = ast.literal_eval(content)
            except Exception:  # noqa
                content = None  # type: ignore
            if isinstance(content, str):
                str_content = repr(content)
    hint = hint.format(obj_name=obj_name, str_content=str_content)
    cause = cause.format(obj_name=obj_name, str_content=str_content)
    return {"cause": cause, "suggest": hint}


def use_builtin_function(
    obj_name: str, attribute: str, known_builtin: str
) -> CauseInfo:
    hint = _("Did you mean `{known_builtin}({obj_name})`?\n").format(
        known_builtin=known_builtin, obj_name=obj_name
    )
    cause = _(
        "The object `{obj_name}` has no attribute named `{attribute}`.\n"
        "Perhaps you can use the Python builtin function `{known_builtin}` instead:\n"
        "`{known_builtin}({obj_name})`."
    ).format(known_builtin=known_builtin, obj_name=obj_name, attribute=attribute)
    return {"cause": cause, "suggest": hint}


def perhaps_join(obj: Any) -> bool:
    return bool(
        hasattr(obj, "__iter__")
        or (hasattr(obj, "__getitem__") and hasattr(obj, "__len__"))
    )


def perhaps_synonym(
    attribute: str, known_attributes: Iterable[str]
) -> Optional[List[str]]:
    synonyms = [
        ["add", "append", "extend", "insert", "push", "update", "union"],
        ["remove", "discard", "pop"],
    ]
    for syn_list in synonyms:
        if attribute in syn_list:
            return [attr for attr in syn_list if attr in known_attributes]
    return None


def use_synonym(obj_name: str, attribute: str, synonyms: Sequence[str]) -> CauseInfo:
    hint = _("Did you mean `{attr}`?\n").format(attr=synonyms[0])
    cause = _("The object `{name}` has no attribute named `{attribute}`.\n").format(
        name=obj_name, attribute=attribute
    )
    if len(synonyms) == 1:
        cause += _(
            "However, `{attr}` is an attribute of `{name}` with a similar meaning.\n"
        ).format(name=obj_name, attr=synonyms[0])
    else:
        cause += _(
            "However, `{name}` has the following attributes with similar meanings:\n"
            "`{attributes}`.\n"
        ).format(name=obj_name, attributes=list_to_string(synonyms))

    return {"cause": cause, "suggest": hint}


def missing_comma(first: str, second: str) -> CauseInfo:
    hint = _("Did you mean to separate object names by a comma?\n")

    cause = _(
        "`{second}` is not an attribute of `{first}`.\n"
        "However, both `{first}` and `{second}` are known objects.\n"
        "Perhaps you wrote a period to separate these two objects, \n"
        "instead of using a comma.\n"
    ).format(first=first, second=second)

    return {"cause": cause, "suggest": hint}