File: name_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 (430 lines) | stat: -rw-r--r-- 15,882 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
import re
from importlib.util import find_spec
from types import FrameType
from typing import Any, Tuple

from .. import debug_helper, info_variables, token_utils, utils
from ..ft_gettext import current_lang
from ..message_parser import get_parser
from ..tb_data import TracebackData  # for type checking only
from ..typing_info import CauseInfo, SimilarNamesInfo  # for type checking only
from ..utils import list_to_string
from . import stdlib_modules, third_party_names
from .modules_attributes import attribute_names

parser = get_parser(NameError)
_ = current_lang.translate


def using_python() -> str:  # pragma: no cover
    return _("You are already using Python!")


# The following is also intended to be used in custom environments;
# we currently use it in Mu.  It is meant to recognize names that
# are intended as a single word command, or call to a function
# that does is not available in a given environment.
CUSTOM_NAMES = {"python": using_python, "python3": using_python}


def is_module_attribute(name):
    if name not in attribute_names:
        return ""
    names = attribute_names[name]
    if len(names) == 1:
        return _(
            "`{name}` is a name found in module `{mod}`.\n"
            "Perhaps you forgot to write\n\n    from {mod} import {name}\n"
        ).format(name=name, mod=names[0])
    return _(
        "`{name}` is a name found in the following modules:\n"
        "{modules}.\n"
        "Perhaps you forgot to import `{name}` from one of these modules.\n"
    ).format(name=name, modules=list_to_string(names))


@parser._add
def free_variable_referenced(message: str, _tb_data: TracebackData) -> CauseInfo:
    pattern = re.compile(
        r"free variable '(.*)' referenced before assignment in enclosing scope"
    )
    pattern3_11 = re.compile(
        r"cannot access free variable '(.*)'"
        + " where it is not associated with a value in enclosing scope"
    )
    match = re.search(pattern, message)
    if not match:
        match = re.search(pattern3_11, message)
    if not match:
        return {}

    unknown_name = match[1]
    cause = _(
        "In your program, `{var_name}` is an unknown name\n"
        "that exists in an enclosing scope,\n"
        "but has not yet been assigned a value.\n"
    ).format(var_name=unknown_name)
    return {"cause": cause}


@parser._add
def name_not_defined(message: str, tb_data: TracebackData) -> CauseInfo:
    pattern = re.compile(r"name '(.*)' is not defined")
    match = re.search(pattern, message)
    if not match:
        return {}

    unknown_name = match[1]
    frame = tb_data.exception_frame
    is_special_name = perhaps_special_name(unknown_name, tb_data)
    if is_special_name:
        return is_special_name

    is_special_keyword = perhaps_special_keyword(
        unknown_name, tb_data.original_bad_line
    )
    if is_special_keyword:
        return is_special_keyword

    cause = _("In your program, no object with the name `{var_name}` exists.\n").format(
        var_name=unknown_name
    )

    # If the unknown name is followed by '.', it is not a typo for a builtin
    potential_module_attribute = "<None>"
    try:
        rest_of_line = tb_data.node.first_token.line[tb_data.node.first_token.end[1] :]
        tokens = token_utils.get_significant_tokens(rest_of_line)
        potential_module = tokens and tokens[0].string == "."
        if len(tokens) > 1:
            potential_module_attribute = tokens[1].string
    except Exception:  # noqa
        potential_module = True
        similar = info_variables.get_similar_names(unknown_name, frame)
    else:
        similar = info_variables.get_similar_names(
            unknown_name, frame, include_builtins=not potential_module
        )

    hint = ""
    if potential_module:
        known_module = is_stdlib_module(unknown_name)
        if known_module:
            cause = known_module["cause"]
            if "suggest" in hint:
                hint = known_module["suggest"]
        else:
            known_module = is_third_party_module(unknown_name)
            if known_module:
                cause = known_module["cause"]
                hint = known_module["suggest"]

    type_hint = info_variables.name_has_type_hint(unknown_name, frame)

    if similar["best"]:
        if hint:
            hint = hint.replace("\n", "") + _(" Or did you mean `{name}`?\n").format(
                name=similar["best"]
            )
        else:
            hint = _("Did you mean `{name}`?\n").format(name=similar["best"])
    elif type_hint and not hint:
        hint = _("Did you use a colon instead of an equal sign?\n")

    additional = type_hint + format_similar_names(unknown_name, similar)
    try:
        more, hint = missing_self(unknown_name, frame, tb_data, hint)
        if more:
            additional += "\n" + more
    except Exception as e:  # pragma: no cover
        debug_helper.log("Problem in name_not_defined()/missing_self().")
        debug_helper.log_error(e)

    forgot_import = is_module_attribute(unknown_name)
    if forgot_import:
        if additional:
            additional += "\n" + forgot_import
        else:
            additional = forgot_import
    if not additional and not hint:
        # example: maths.pi
        name = typo_in_stdlib_module(unknown_name, potential_module_attribute)
        if name:
            additional = "\n" + _(
                "Perhaps you meant to write `{name}` and also forgot to import it.\n"
            ).format(name=name)
        else:
            additional = _("I have no additional information for you.\n")

    explanation = {"cause": cause + additional}
    if not hint:
        return explanation
    explanation["suggest"] = hint
    return explanation


def perhaps_special_name(name: str, tb_data: TracebackData) -> CauseInfo:
    if name == "ꓺ":  # pragma: no cover
        return flipfloperator()
    if name == "__debug__" and tb_data.bad_line.startswith("del "):
        return delete_debug()
    if name in {"i", "j"}:
        hint = _("Did you mean `1j`?\n")
        cause = _(
            "In your program, no object with the name `{var_name}` exists.\n"
        ).format(var_name=name)
        cause += _(
            "However, sometimes `{name}` is intended to represent\n"
            "the square root of `-1` which is written as `1j` in Python.\n"
        ).format(name=name)
        return {"cause": cause, "suggest": hint}
    if name in CUSTOM_NAMES:
        bad_line = tb_data.bad_line.replace("(", "").replace(")", "").strip()
        if bad_line == name:
            cause = CUSTOM_NAMES[name]()
            return {"cause": cause, "suggest": cause}
    return {}


def perhaps_special_keyword(word_with_typo: str, bad_line: str) -> CauseInfo:
    """Identifies if one of 'pass', 'break', and 'continue' has possibly been misspelled.
    For 'break' and 'continue', it verifies that it would be used on an indented line since
    not doing so would definitely result in a SyntaxError.
    """
    tokens = token_utils.get_significant_tokens(bad_line)
    if len(tokens) != 1:
        return {}
    token = tokens[0]
    similar = utils.get_similar_words(word_with_typo, ["pass", "continue", "break"])
    if not similar:
        return {}
    correct_word = similar[0]
    if correct_word in ["continue", "break"] and token.start_col == 0:
        # continue and break need to be part of a block. We do not want
        # to make a suggestion that would result in a SyntaxError
        return {}
    hint = _("Did you mean `{word}`?\n").format(word=correct_word)
    cause = _(
        "I suspect you meant to write the keyword `{word}` and made a typo.\n"
    ).format(word=correct_word)
    return {"cause": cause, "suggest": hint}


def delete_debug() -> CauseInfo:
    # https://bugs.python.org/issue45000
    hint = _("`__debug__` is a constant.\n")
    cause = _(
        "`__debug__` is a constant that cannot be deleted.\n"
        "In future Python versions, attempting to delete it will be a SyntaxError.\n"
    )
    return {"cause": cause, "suggest": hint}


def flipfloperator() -> CauseInfo:  # pragma: no cover
    hint = _("You must be a fan of PyConAu!\n")
    cause = _(
        "I am guessing that you tried to use (one of) the flipfloperators\n"
        "shown during the second Lightning Talk session of PyConAu 2018,\n"
        "but that you forgot to install the module from PyPI.\n\n"
        "#### Note that it is still a bad idea.\n"
    )
    return {"cause": cause, "suggest": hint}


def is_stdlib_module(name: str) -> CauseInfo:
    """Determine if an unknown name is to be found in the Python standard library.
    We're looking for something like name.attribute"""
    # Some Python 2 libraries used names with uppercase letters.
    lowercase = name.lower()
    if stdlib_modules.module_exists(name) or stdlib_modules.module_exists(lowercase):
        hint = _("Did you forget to import `{name}`?\n").format(name=lowercase)
        cause = (
            "\n"
            + _(
                "The name `{name}` is not defined in your program.\n"
                "Perhaps you forgot to import `{lowercase}` which is found\n"
                "in Python's standard library.\n"
            ).format(name=name, lowercase=lowercase)
            + "\n"
        )
        if name != lowercase:
            cause += (
                _(
                    "Note that the name of the module is `{lowercase}` and not `{name}`.\n"
                ).format(lowercase=lowercase, name=name)
                + "\n"
            )
            return {"cause": cause, "suggest": hint, "lowercase": True}
        return {"cause": cause, "suggest": hint}
    if name in stdlib_modules.names:
        return {
            "cause": _(
                "There is a module named `{name}` that is part of the\n"
                "Python standard library. However, it might not be available\n"
                "for your operating system, or it may to be installed separately.\n"
            ).format(name=name)
            + "\n\n"
        }
    return {}


def typo_in_stdlib_module(name, attribute):
    similar = utils.get_similar_words(name, stdlib_modules.names)
    for other in similar:
        if (
            stdlib_modules.module_exists(other)
            and attribute in attribute_names
            and other in attribute_names[attribute]
        ):
            return other
    return None


def is_third_party_module(name: str) -> CauseInfo:
    found = find_spec(name)
    if found:
        hint = _("Did you forget to import `{name}`?\n").format(name=name)
        cause = (
            "\n"
            + _(
                "The name `{name}` is not defined in your program.\n"
                "Perhaps you forgot to import `{name}` which is a known library.\n"
            ).format(name=name)
            + "\n"
        )
        return {"cause": cause, "suggest": hint}

    synonyms = third_party_names.module_synonyms
    if name in synonyms:
        hint = _("Did you forget to import `{true_name}`?\n").format(
            true_name=synonyms[name]
        )
        cause = (
            "\n"
            + _(
                "The name `{name}` is not defined in your program.\n"
                "Perhaps you forgot to write\n\n"
                "    import {true_name} as {name}\n"
            ).format(name=name, true_name=synonyms[name])
            + "\n"
        )
        return {"cause": cause, "suggest": hint}
    return {}


def format_similar_names(name: str, similar: SimilarNamesInfo) -> str:
    """This function formats the names that were found to be similar"""
    nb_similar_names = (
        len(similar["locals"]) + len(similar["globals"]) + len(similar["builtins"])
    )
    if nb_similar_names == 0:
        return ""

    found_local = _("The similar name `{name}` was found in the local scope.\n")
    found_global = _("The similar name `{name}` was found in the global scope.\n")
    builtin_similar = _("The Python builtin `{name}` has a similar name.\n")

    if nb_similar_names == 1:
        if similar["locals"]:
            return found_local.format(name=similar["locals"][0])
        if similar["globals"]:
            return found_global.format(name=similar["globals"][0])
        return builtin_similar.format(name=similar["builtins"][0])

    message = _(
        "Instead of writing `{name}`, perhaps you meant one of the following:\n"
    ).format(name=name)

    for scope, pre in (
        ("locals", _("*   Local scope: ")),
        ("globals", _("*   Global scope: ")),
        ("builtins", _("*   Python builtins: ")),
    ):
        if similar[scope]:
            message += pre + str(similar[scope])[1:-1].replace("'", "`") + "\n"

    return message


def missing_self(
    unknown_name: str, frame: FrameType, tb_data: TracebackData, hint: str
) -> Tuple[str, str]:
    """If the unknown name is referred to with no '.' before it,
    and is an attribute of a known object, perhaps 'self.'
    is missing."""
    message = ""
    try:
        bad_statement = utils.get_bad_statement(tb_data)
        tokens = token_utils.get_significant_tokens(bad_statement)
    except Exception:  # noqa  # pragma: no cover
        debug_helper.log(
            "Exception raised in missing_self() while trying to get tokens"
        )
        return message, hint

    if not tokens:  # pragma: no cover
        return message, hint

    prev_token = tokens[0]
    for index, token in enumerate(tokens):
        if token == unknown_name and prev_token != ".":
            break
        prev_token = token
    else:
        return message, hint

    first_arg_self = (
        len(tokens) > index + 3
        and tokens[index + 1] == "("
        and tokens[index + 2] == "self"
    )

    env = (("local", frame.f_locals), ("global", frame.f_globals))

    for scope, dict_ in env:
        names = info_variables.get_variables_in_frame_by_scope(frame, scope)
        dict_copy = dict(dict_)
        for name in names:
            if name in dict_copy:
                obj = dict_copy[name]
                known_attributes = dir(obj)
                if unknown_name in known_attributes:
                    return missing_self_cause(
                        name, unknown_name, obj, scope, first_arg_self, hint
                    )
    return message, hint


def missing_self_cause(
    name: str, unknown_name: str, obj: Any, scope: str, first_arg_self: bool, hint: str
) -> Tuple[str, str]:
    obj_repr = repr(obj)
    if obj_repr.startswith("<") and obj_repr.endswith(">"):
        obj_repr = info_variables.simplify_repr(obj_repr, splitlines=False)
    if first_arg_self and name == "self":
        suggest = _("Did you write `self` at the wrong place?\n")
        message = _(
            "The {scope} object `{obj}`\n"
            "has an attribute named `{unknown_name}`.\n"
            "Perhaps you should have written `self.{unknown_name}(...`\n"
            "instead of `{unknown_name}(self, ...`.\n"
        ).format(scope=scope, obj=obj_repr, unknown_name=unknown_name)
    elif name == "self":
        suggest = _("Did you forget to add `self.`?\n")
        message = _(
            "A {scope} object, `{obj}`,\n"
            "has an attribute named `{unknown_name}`.\n"
            "Perhaps you should have written `self.{unknown_name}`\n"
            "instead of `{unknown_name}`.\n"
        ).format(scope=scope, obj=obj_repr, unknown_name=unknown_name)
    else:
        suggest = _("Did you forget to add `{name}.`?\n").format(name=name)
        message = _(
            "The {scope} object `{name}`\n"
            "has an attribute named `{unknown_name}`.\n"
            "Perhaps you should have written `{name}.{unknown_name}`\n"
            "instead of `{unknown_name}`.\n"
        ).format(scope=scope, name=name, unknown_name=unknown_name)

    hint += suggest
    return message, hint