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
|
"""This module includes all relevant classes and functions so that
friendly-traceback can give help with warnings.
It contains one function (``enable_warnings``) which is part of the API,
and one (``get_warning_parser``) which is the only other function in this module
which is intended to be part of the public API. However, while the latter
can be imported using ``from friendly_traceback import enable_warnings``,
``get_warning_parser`` needs to be imported from this module.
"""
import inspect
import warnings
from importlib import import_module
from typing import List, Type
import executing
from stack_data import BlankLines, Formatter, Options
from .config import session
from .frame_info import FriendlyFormatter
from .ft_gettext import current_lang, internal_error
from .info_generic import get_generic_explanation
from .info_variables import get_var_info
from .path_info import path_utils
from .typing_info import _E, CauseInfo, Parser
_ = current_lang.translate
_warnings_seen = {}
_RUNNING_TESTS = False
IGNORE_WARNINGS = set()
def enable_warnings(testing: bool = False) -> None:
"""Used to enable all warnings, with 'always' being used as the
parameter for warnings.simplefilter.
While friendly_traceback, used by many third-party packages, does not
automatically handle warnings by default, friendly, which is meant to
be used by end-users instead of other packages/libraries, does call
enable_warnings by default.
"""
# Note: enable_warnings is imported by friendly_traceback.__init__
# so that it is part of the public API.
global _RUNNING_TESTS
_RUNNING_TESTS = testing
warnings.simplefilter("always")
warnings.showwarning = show_warning
class MyFormatter(Formatter):
def format_frame(self, frame):
yield from super().format_frame(frame)
class WarningInfo:
def __init__(
self, warning_instance, warning_type, filename, lineno, frame=None, lines=None
):
self.warning_instance = warning_instance
self.message = str(warning_instance)
self.warning_type = warning_type
self.filename = filename
self.lineno = lineno
self.begin_lineno = lineno
self.lines = lines
self.frame = frame
self.info = {"warning_message": f"{warning_type.__name__}: {self.message}\n"}
self.info["message"] = self.info["warning_message"]
if frame is not None:
source = self.format_source()
self.info["warning_source"] = source
self.problem_statement = executing.Source.executing(frame).text()
var_info = get_var_info(self.problem_statement, frame)
self.info["warning_variables"] = var_info["var_info"]
if "additional_variable_warning" in var_info:
self.info["additional_variable_warning"] = var_info[
"additional_variable_warning"
]
else:
self.info["warning_source"] = self.get_source_frame_missing()
self.recompile_info()
def recompile_info(self):
self.info["lang"] = session.lang
self.info["generic"] = get_generic_explanation(self.warning_type)
short_filename = path_utils.shorten_path(self.filename)
if "[" in short_filename:
location = _(
"Warning issued on line `{line}` of code block {filename}."
).format(filename=short_filename, line=self.lineno)
else:
location = _(
"Warning issued on line `{line}` of file '{filename}'."
).format(filename=short_filename, line=self.lineno)
self.info["warning_location_header"] = location + "\n"
self.info.update(**get_warning_cause(self.warning_type, self.message, self))
def format_source(self):
nb_digits = len(str(self.lineno))
lineno_fmt_string = "{:%d}| " % nb_digits # noqa
line_gap_string = " " * nb_digits + "(...)"
line_number_gap_string = " " * (nb_digits - 1) + ":"
formatter = FriendlyFormatter(
options=Options(blank_lines=BlankLines.SINGLE, before=2),
line_number_format_string=lineno_fmt_string,
line_gap_string=line_gap_string,
line_number_gap_string=line_number_gap_string,
)
formatted = formatter.format_frame(self.frame)
return "".join(list(formatted)[1:])
def get_source_frame_missing(self):
new_lines = []
try:
source = executing.Source.for_filename(self.filename)
statement = source.statements_at_line(self.lineno).pop()
lines = source.lines[statement.lineno - 1 : statement.end_lineno]
for number, line in enumerate(lines, start=statement.lineno):
if number == self.lineno:
new_lines.append(f" -->{number}| {line}")
else:
new_lines.append(f" {number}| {line}")
self.problem_statement = "".join(lines)
return "\n".join(new_lines)
except Exception:
self.problem_statement = None
# self.lines comes from Python; it should correspond to a single logical line
# but is sometimes seemingly split in two parts.
self.problem_statement = "".join(
self.lines if self.lines is not None else []
)
return (
f" -->{self.lineno}| {self.problem_statement}"
if self.problem_statement
else _(
"The source is unavailable.\n"
"If you used `exec`, consider using `friendly_exec` instead."
)
)
def saw_warning_before(warning_type, message, filename, lineno) -> bool:
"""Records a warning if it has not been seen at the exact location
and returns True; returns False otherwise.
"""
# Note: unlike show_warning whose API is dictated by Python,
# we order the argument in some grouping that seems more logical
# for the recorded structure
if warning_type in _warnings_seen:
if message in _warnings_seen[warning_type]:
if filename in _warnings_seen[warning_type][message]:
if lineno in _warnings_seen[warning_type][message][filename]:
return True
_warnings_seen[warning_type][message][filename].append(lineno)
else:
_warnings_seen[warning_type][message][filename] = [lineno]
else:
_warnings_seen[warning_type][message] = {filename: [lineno]}
else:
_warnings_seen[warning_type] = {message: {}}
_warnings_seen[warning_type][message][filename] = [lineno]
return False
def show_warning(
warning_instance, warning_type, filename, lineno, file=None, line=None
):
for do_not_show_warning in IGNORE_WARNINGS:
if do_not_show_warning(warning_instance, warning_type, filename, lineno):
return
if saw_warning_before(
warning_type.__name__, str(warning_instance), filename, lineno
):
# Avoid showing the same warning if it occurs in a loop, or in
# other way in which a given instruction that give rise to a warning
# is repeated
return
try:
for outer_frame in inspect.getouterframes(inspect.currentframe()):
if outer_frame.filename == filename and outer_frame.lineno == lineno:
warning_data = WarningInfo(
warning_instance,
warning_type,
filename,
lineno,
frame=outer_frame.frame,
lines=outer_frame.code_context,
)
break
else:
warning_data = WarningInfo(warning_instance, warning_type, filename, lineno)
except Exception:
warning_data = WarningInfo(warning_instance, warning_type, filename, lineno)
message = str(warning_instance)
if not _RUNNING_TESTS:
session.recorded_tracebacks.append(warning_data)
elif "cause" in warning_data.info:
# We know how to explain this; we do not print while running tests
return
session.write_err(f"`{warning_type.__name__}`: {message}\n")
INCLUDED_PARSERS = {
SyntaxWarning: "syntax_warning",
}
WARNING_DATA_PARSERS = {}
class WarningDataParser:
"""This class is used to create objects that collect message parsers."""
def __init__(self) -> None:
self.parsers: List[Parser] = []
self.core_parsers: List[Parser] = []
self.custom_parsers: List[Parser] = []
def _add(self, func: Parser) -> None:
"""This method is meant to be used only within friendly-traceback.
It is used as a decorator to add a message parser to a list that is
automatically updated.
"""
self.parsers.append(func)
self.core_parsers.append(func)
def add(self, func: Parser) -> None:
"""This method is meant to be used by projects that extend
friendly-traceback. It is used as a decorator to add a message parser
to a list that is automatically updated::
@instance.add
def some_warning_parsers(message, traceback_data):
....
"""
self.custom_parsers.append(func)
self.parsers = self.custom_parsers + self.core_parsers
def get_warning_parser(warning_type: Type[_E]) -> WarningDataParser:
"""Gets a 'parser' to find the cause for a given warning.
Args:
warning_type: a warning class.
Usage::
parser = get_warning_parser(SomeSpecificWarning)
@parser.add
def some_meaningful_name(warning_message: str,
warning_data: WarningDataParser) -> dict:
if not handled_by_this_function(warning_message):
return {} # let other parsers deal with it
...
"""
if warning_type not in WARNING_DATA_PARSERS:
WARNING_DATA_PARSERS[warning_type] = WarningDataParser()
if warning_type in INCLUDED_PARSERS:
base_path = "friendly_traceback.warning_parsers."
import_module(base_path + INCLUDED_PARSERS[warning_type])
return WARNING_DATA_PARSERS[warning_type]
def get_warning_cause(
warning_type,
message: str,
warning_data: WarningDataParser = None,
) -> CauseInfo:
"""Attempts to get the likely cause of an exception."""
try:
return get_cause(warning_type, message, warning_data)
except Exception as e: # noqa # pragma: no cover
session.write_err("Exception raised")
session.write_err(str(e))
session.write_err(internal_error(e))
return {}
def get_cause(
warning_type,
message: str,
warning_data: WarningDataParser,
) -> CauseInfo:
"""For a given exception type, cycle through the known message parsers,
looking for one that can find a cause of the exception."""
warning_parsers = get_warning_parser(warning_type)
for parser in warning_parsers.parsers:
# This could be simpler if we could use the walrus operator
cause = parser(message, warning_data)
if cause:
return cause
return {}
|