File: import_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 (198 lines) | stat: -rw-r--r-- 7,470 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
"""Getting specific information for ImportError"""

import re
import sys
from typing import List, Optional, Tuple

from .. import debug_helper
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

parser = get_parser(ImportError)
_ = current_lang.translate


@parser._add
def partially_initialized_module(message: str, tb_data: TracebackData) -> CauseInfo:
    # Python 3.8+
    pattern = re.compile(
        r"cannot import name '(.*)' from partially initialized module '(.*)'"
    )
    match = re.search(pattern, message)
    if not match:
        return {}
    if "circular import" in message:
        return cannot_import_name_from(
            match[1], match[2], tb_data, add_circular_hint=False
        )
    # I thought I saw such a case where "circular import" was not added
    # but have not been able to find it again.
    return cannot_import_name_from(match[1], match[2], tb_data)  # pragma: no cover


@parser._add
def _cannot_import_name_from(message: str, tb_data: TracebackData) -> CauseInfo:
    # Python 3.7+
    pattern = re.compile(r"cannot import name '(.*)' from '(.*)'")
    match = re.search(pattern, message)
    return cannot_import_name_from(match[1], match[2], tb_data) if match else {}


@parser._add
def _cannot_import_name(message: str, tb_data: TracebackData) -> CauseInfo:
    # Python 3.6 does not give us more information
    pattern = re.compile(r"cannot import name '(.*)'")
    match = re.search(pattern, message)
    return cannot_import_name(match[1], tb_data) if match else {}


def cannot_import_name_from(
    name: str, module: str, tb_data: TracebackData, add_circular_hint: bool = True
) -> CauseInfo:
    hint = None
    circular_info = None

    modules_imported = extract_import_data_from_traceback(tb_data)
    if modules_imported:
        circular_info = find_circular_import(modules_imported)
        if circular_info and add_circular_hint:
            hint = _("You have a circular import.\n")
            # Python 3.8+ adds a similar hint on its own.

    cause = _(
        "The object that could not be imported is `{name}`.\n"
        "The module or package where it was \n"
        "expected to be found is `{module}`.\n"
    ).format(name=name, module=module)

    if circular_info:
        if hint is None:
            return {"cause": cause + "\n" + circular_info}

        return {"cause": cause + "\n" + circular_info, "suggest": hint}

    if not add_circular_hint:  # pragma: no cover
        debug_helper.log("New example to add")
        return {
            "cause": cause
            + "\n"
            + _(
                "Python indicated that you have a circular import.\n"
                "This can occur if executing the code in module 'A'\n"
                "results in executing the code in module 'B' where\n"
                "an attempt to import a name from module 'A' is made\n"
                "before the execution of the code in module 'A' had been completed.\n"
            )
        }

    try:
        mod = sys.modules[module]
    except Exception:  # noqa  # pragma: no cover
        cause += "\n" + _(
            "Inconsistent state: `'{module}'` was apparently not imported.\n"
            "As a result, no further analysis can be done.\n"
        ).format(module=module)
        return {"cause": cause}

    similar = get_similar_words(name, dir(mod))
    if not similar:
        return {"cause": cause}

    if len(similar) == 1:
        hint = _("Did you mean `{name}`?\n").format(name=similar[0])
        cause = _(
            "Perhaps you meant to import `{correct}` (from `{module}`) "
            "instead of `{typo}`\n"
        ).format(correct=similar[0], typo=name, module=module)
    else:
        candidates = list_to_string(similar)
        hint = _("Did you mean one of the following: `{names}`?\n").format(
            names=candidates
        )
        cause = _(
            "Instead of trying to import `{typo}` from `{module}`, \n"
            "perhaps you meant to import one of \n"
            "the following names which are found in module `{module}`:\n"
            "`{candidates}`\n"
        ).format(candidates=candidates, typo=name, module=module)

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


def cannot_import_name(name: str, tb_data: TracebackData) -> CauseInfo:
    # Python 3.6 does not give us the name of the module
    pattern = re.compile(r"from (.*) import")
    match = re.search(pattern, tb_data.bad_line)

    if not match:  # pragma: no cover
        debug_helper.log("New example to consider.")
        return {
            "cause": _("The object that could not be imported is `{name}`.\n").format(
                name=name
            )
            + please_report()
        }

    return cannot_import_name_from(name, match[1], tb_data)


Modules = List[Tuple[str, str]]


def extract_import_data_from_traceback(tb_data: TracebackData) -> Modules:
    """Attempts to extract the list of imported modules from the traceback information"""
    pattern_file = re.compile(r'^File "(.*)", line', re.M)
    pattern_from = re.compile(r"^from (.*) import", re.M)
    pattern_import = re.compile(r"^import (.*)", re.M)
    modules_imported = []
    tb_lines = tb_data.simulated_python_traceback.split("\n")
    current_file = ""
    for line in tb_lines:
        line = line.strip()
        match_file = re.search(pattern_file, line)
        match_from = re.search(pattern_from, line)
        match_import = re.search(pattern_import, line)

        if match_file:
            current_file = path_utils.shorten_path(match_file[1])
        elif match_from or match_import:
            if match_from:
                modules_imported.append((current_file, match_from[1]))
            else:
                module = match_import[1]
                if "," in module:  # multiple modules imported on same line
                    modules = module.split(",")
                    for mod in modules:
                        modules_imported.append(
                            (current_file, mod.replace("(", "").strip())
                        )
                else:
                    modules_imported.append((current_file, module))
            current_file = ""

    return modules_imported


def find_circular_import(modules_imported: Modules) -> Optional[str]:
    """This attempts to find circular imports."""
    last_file, last_module = modules_imported[-1]

    for file, module in modules_imported[:-1]:
        if module == last_module:
            return _(
                "The problem was likely caused by what is known as a 'circular import'.\n"
                "First, Python imported and started executing the code in file\n"
                "   '{file}'.\n"
                "which imports module `{last_module}`.\n"
                "During this process, the code in another file,\n"
                "   '{last_file}'\n"
                "was executed. However in this last file, an attempt was made\n"
                "to import the original module `{last_module}`\n"
                "a second time, before Python had completed the first import.\n"
            ).format(
                file=file, last_file=last_file, module=module, last_module=last_module
            )