Source code for friendly_traceback.about_warnings

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


[docs]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
[docs]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 {}