aboutsummaryrefslogtreecommitdiff
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------

# pylint: disable=protected-access

"""A file for custom traceback for removing file path from the trace for compliance need."""

import os
import sys
import traceback
import types
from typing import Any, Generator, List, Optional, Type


class _CustomStackSummary(traceback.StackSummary):
    """Subclass of StackSummary."""

    def format(self) -> List[str]:
        """Format the stack ready for printing.

        :return: A list of strings ready for printing.
          * Each string in the resulting list corresponds to a single frame from the stack.
          * Each string ends in a newline; the strings may contain internal newlines as well, for those items with
            source text lines.
        :rtype: List[str]
        """
        result = []
        for frame in self:
            row = ['  File "{}", line {}, in {}\n'.format(os.path.basename(frame.filename), frame.lineno, frame.name)]
            if frame.line:
                row.append("    {}\n".format(frame.line.strip()))
            if frame.locals:
                for name, value in sorted(frame.locals.items()):
                    row.append("    {name} = {value}\n".format(name=name, value=value))
            result.append("".join(row))
        return result


# pylint: disable=too-many-instance-attributes
class _CustomTracebackException(traceback.TracebackException):
    # pylint: disable=super-init-not-called
    def __init__(
        self, exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, _seen=None
    ):
        if _seen is None:
            _seen = set()
        _seen.add(exc_value)
        # Gracefully handle (the way Python 2.4 and earlier did) the case of
        # being called with no type or value (None, None, None).
        if exc_value and exc_value.__cause__ is not None and exc_value.__cause__ not in _seen:
            cause = _CustomTracebackException(
                type(exc_value.__cause__),
                exc_value.__cause__,
                exc_value.__cause__.__traceback__,
                limit=limit,
                lookup_lines=False,
                capture_locals=capture_locals,
                _seen=_seen,
            )
        else:
            cause = None
        if exc_value and exc_value.__context__ is not None and exc_value.__context__ not in _seen:
            context = _CustomTracebackException(
                type(exc_value.__context__),
                exc_value.__context__,
                exc_value.__context__.__traceback__,
                limit=limit,
                lookup_lines=False,
                capture_locals=capture_locals,
                _seen=_seen,
            )
        else:
            context = None
        self.exc_traceback = exc_traceback
        self.__cause__ = cause
        self.__context__ = context
        self.__suppress_context__ = exc_value.__suppress_context__ if exc_value else False
        # TODO: locals.
        self.stack = _CustomStackSummary.extract(
            traceback.walk_tb(exc_traceback),
            limit=limit,
            lookup_lines=lookup_lines,
            capture_locals=capture_locals,
        )
        self.exc_type = exc_type
        # Capture now to permit freeing resources: only complication is in the
        # unofficial API _format_final_exc_line
        self._str = traceback._some_str(exc_value)
        if exc_type and issubclass(exc_type, SyntaxError):
            # Handle SyntaxError's specially
            self.filename = exc_value.filename
            self.lineno = str(exc_value.lineno)
            self.text = exc_value.text
            self.offset = exc_value.offset
            self.msg = exc_value.msg
        if lookup_lines:
            self._load_lines()

    def format_exception_only(self) -> Generator:
        """Format the exception part of the traceback.

        :return: An iterable of strings, each ending in a newline. Normally, the generator emits a single
           string; however, for SyntaxError exceptions, it emits several lines that (when printed) display
           detailed information about where the syntax error occurred. The message indicating which exception occurred
           is always the last string in the output.
        :rtype: Iterable[str]
        """
        if self.exc_type is None:
            yield traceback._format_final_exc_line(None, self._str)
            return

        stype = self.exc_type.__qualname__
        smod = self.exc_type.__module__
        if smod not in ("__main__", "builtins"):
            stype = smod + "." + stype

        if not issubclass(self.exc_type, SyntaxError):
            yield traceback._format_final_exc_line(stype, self._str)  # type: ignore[attr-defined]
            return

        # It was a syntax error; show exactly where the problem was found.
        filename = os.path.basename(self.filename) or "<string>"
        lineno = str(self.lineno) or "?"
        yield '  File "{}", line {}\n'.format(filename, lineno)

        badline = self.text
        offset = self.offset
        if badline is not None:
            yield "    {}\n".format(badline.strip())
            if offset is not None:
                caretspace: Any = badline.rstrip("\n")
                offset = min(len(caretspace), offset) - 1
                caretspace = caretspace[:offset].lstrip()
                # non-space whitespace (likes tabs) must be kept for alignment
                caretspace = ((c.isspace() and c or " ") for c in caretspace)
                yield "    {}^\n".format("".join(caretspace))
        msg = self.msg or "<no detail available>"
        yield "{}: {}\n".format(stype, msg)


def format_exc(limit: Optional[int] = None, chain: bool = True) -> str:
    """Like print_exc() but return a string.

    :param limit: None to include all frames or the number of frames to include.
    :type limit: Optional[int]
    :param chain: Whether to format __cause__ and __context__. Defaults to True
    :type chain: bool
    :return: The formatted exception string
    :rtype: str
    """
    return "".join(format_exception(*sys.exc_info(), limit=limit, chain=chain))  # type: ignore[misc]


def format_exception(  # pylint: disable=unused-argument
    etype: Type[BaseException],
    value: BaseException,
    tb: types.TracebackType,
    limit: Optional[int] = None,
    chain: bool = True,
) -> List[str]:
    """Format a stack trace and the exception information.

    The arguments have the same meaning as the corresponding arguments to print_exception().

    :param etype: The type of the exception
    :type etype: Type[BaseException]
    :param value: The exception
    :type value: BaseException
    :param tb: The exception traceback
    :type tb: types.TracebackType
    :param limit: None to include all frames or the number of frames to include.
    :type limit: Optional[int]
    :param chain: Whether to format __cause__ and __context__. Defaults to True
    :type chain: bool
    :return: A list of strings, each ending in a newline and some containing internal newlines.
      When these lines are concatenated and printed, exactly the same text is printed as does print_exception().
    :rtype: List[str]
    """
    # format_exception has ignored etype for some time, and code such as cgitb
    # passes in bogus values as a result. For compatibility with such code we
    # ignore it here (rather than in the new TracebackException API).
    return list(_CustomTracebackException(type(value), value, tb, limit=limit).format(chain=chain))