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
)
|