############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """An exception formatter that shows traceback supplements and traceback info, optionally in HTML. """ import sys try: from html import escape except ImportError: # pragma: PY2 from cgi import escape import linecache import traceback DEBUG_EXCEPTION_FORMATTER = 1 class TextExceptionFormatter(object): line_sep = '\n' def __init__(self, limit=None, with_filenames=False): self.limit = limit self.with_filenames = with_filenames def escape(self, s): return s def getPrefix(self): return 'Traceback (most recent call last):' def getLimit(self): limit = self.limit if limit is None: limit = getattr(sys, 'tracebacklimit', 200) return limit def formatSupplementLine(self, line): result = ' - %s' % line if not isinstance(result, str): # pragma: PY2 # Must be an Python 2, and must be a unicode `line` # and we upconverted the result to a unicode result = result.encode('utf-8') return result def formatSourceURL(self, url): return [self.formatSupplementLine(url)] def formatSupplement(self, supplement, tb): result = [] fmtLine = self.formatSupplementLine url = getattr(supplement, 'source_url', None) if url is not None: result.extend(self.formatSourceURL(url)) line = getattr(supplement, 'line', 0) if line == -1: line = tb.tb_lineno col = getattr(supplement, 'column', -1) if line: if col is not None and col >= 0: result.append(fmtLine('Line %s, Column %s' % ( line, col))) else: result.append(fmtLine('Line %s' % line)) elif col is not None and col >= 0: result.append(fmtLine('Column %s' % col)) expr = getattr(supplement, 'expression', None) if expr: result.append(fmtLine('Expression: %s' % expr)) warnings = getattr(supplement, 'warnings', None) if warnings: for warning in warnings: result.append(fmtLine('Warning: %s' % warning)) getInfo = getattr(supplement, 'getInfo', None) if getInfo is not None: try: extra = getInfo() if extra: result.append(self.formatSupplementInfo(extra)) except Exception: # pragma: no cover if DEBUG_EXCEPTION_FORMATTER: traceback.print_exc() # else just swallow the exception. return result def formatSupplementInfo(self, info): return self.escape(info) def formatTracebackInfo(self, tbi): return self.formatSupplementLine('__traceback_info__: %s' % (tbi, )) def formatLine(self, tb=None, f=None): if tb and not f: f = tb.tb_frame lineno = tb.tb_lineno elif not tb and f: lineno = f.f_lineno else: raise ValueError("Pass exactly one of tb or f") co = f.f_code filename = co.co_filename name = co.co_name f_locals = f.f_locals f_globals = f.f_globals if self.with_filenames: s = ' File "%s", line %d' % (filename, lineno) else: modname = f_globals.get('__name__', filename) s = ' Module %s, line %d' % (modname, lineno) s = s + ', in %s' % name result = [] result.append(self.escape(s)) # Append the source line, if available line = linecache.getline(filename, lineno) if line: result.append(" " + self.escape(line.strip())) # Output a traceback supplement, if any. if '__traceback_supplement__' in f_locals: # Use the supplement defined in the function. tbs = f_locals['__traceback_supplement__'] elif '__traceback_supplement__' in f_globals: # Use the supplement defined in the module. # This is used by Scripts (Python). tbs = f_globals['__traceback_supplement__'] else: tbs = None if tbs is not None: factory = tbs[0] args = tbs[1:] try: supp = factory(*args) result.extend(self.formatSupplement(supp, tb)) except Exception: # pragma: no cover if DEBUG_EXCEPTION_FORMATTER: traceback.print_exc() # else just swallow the exception. try: tbi = f_locals.get('__traceback_info__', None) if tbi is not None: result.append(self.formatTracebackInfo(tbi)) except Exception: # pragma: no cover if DEBUG_EXCEPTION_FORMATTER: traceback.print_exc() # else just swallow the exception. return self.line_sep.join(result) def formatExceptionOnly(self, etype, value): # We don't want to get an error when we format an error, so we # compensate in our code. For example, on Python 3.11.0 HTTPError # gives an unhelpful KeyError in tempfile when Python formats it. # See https://github.com/python/cpython/issues/90113 try: result = ''.join(traceback.format_exception_only(etype, value)) except Exception: # pragma: no cover # This code branch is only covered on Python 3.11. result = str(value) return result def formatLastLine(self, exc_line): return self.escape(exc_line) def formatException(self, etype, value, tb): # The next line provides a way to detect recursion. The 'noqa' # comment disables a flake8 warning about the unused variable. __exception_formatter__ = 1 # noqa result = [] while tb is not None: if tb.tb_frame.f_locals.get('__exception_formatter__'): # Stop recursion. result.append('(Recursive formatException() stopped, ' 'trying traceback.format_tb)\n') result.extend(traceback.format_tb(tb)) break line = self.formatLine(tb=tb) result.append(line + '\n') tb = tb.tb_next template = ( '...\n' '{omitted} entries omitted, because limit is {limit}.\n' 'Set sys.tracebacklimit or {klass}.limit to a higher' ' value to see omitted entries\n' '...') self._obeyLimit(result, template) result = [self.getPrefix() + '\n'] + result exc_line = self.formatExceptionOnly(etype, value) result.append(self.formatLastLine(exc_line)) return result def extractStack(self, f=None): if f is None: try: raise ZeroDivisionError except ZeroDivisionError: f = sys.exc_info()[2].tb_frame.f_back # The next line provides a way to detect recursion. The 'noqa' # comment disables a flake8 warning about the unused variable. __exception_formatter__ = 1 # noqa result = [] while f is not None: if f.f_locals.get('__exception_formatter__'): # Stop recursion. result.append('(Recursive extractStack() stopped, ' 'trying traceback.format_stack)\n') res = traceback.format_stack(f) res.reverse() result.extend(res) break line = self.formatLine(f=f) result.append(line + '\n') f = f.f_back self._obeyLimit( result, '...{omitted} entries omitted, because limit is {limit}...\n') result.reverse() return result def _obeyLimit(self, result, template): limit = self.getLimit() if limit is not None and len(result) > limit: # cut out the middle part of the TB tocut = len(result) - limit middle = len(result) // 2 lower = middle - tocut // 2 msg = template.format( omitted=tocut, limit=limit, klass=self.__class__.__name__) result[lower:lower + tocut] = [msg] class HTMLExceptionFormatter(TextExceptionFormatter): line_sep = '
\r\n' def escape(self, s): if not isinstance(s, str): try: # pragma: PY2 s = str(s) except UnicodeError: # pragma: PY2 if hasattr(s, 'encode'): # We probably got a unicode string on Python 2. s = s.encode('utf-8') else: # pragma: no cover raise return escape(s, quote=False) def getPrefix(self): return '

Traceback (most recent call last):

\r\n

%s

' % self.escape(exc_line) return line.replace('\n', self.line_sep) def format_exception(t, v, tb, limit=None, as_html=False, with_filenames=False): """Format a stack trace and the exception information. Similar to 'traceback.format_exception', but adds supplemental information to the traceback and accepts two options, 'as_html' and 'with_filenames'. The result is a list of native strings; on Python 2 they are UTF-8 encoded if need be. """ if as_html: fmt = HTMLExceptionFormatter(limit, with_filenames) else: fmt = TextExceptionFormatter(limit, with_filenames) return fmt.formatException(t, v, tb) def print_exception(t, v, tb, limit=None, file=None, as_html=False, with_filenames=True): """Print exception up to 'limit' stack trace entries from 'tb' to 'file'. Similar to 'traceback.print_exception', but adds supplemental information to the traceback and accepts two options, 'as_html' and 'with_filenames'. """ if file is None: # pragma: no cover file = sys.stderr lines = format_exception(t, v, tb, limit, as_html, with_filenames) for line in lines: file.write(line) def extract_stack(f=None, limit=None, as_html=False, with_filenames=True): """Format a stack trace and the exception information. Similar to 'traceback.extract_stack', but adds supplemental information to the traceback and accepts two options, 'as_html' and 'with_filenames'. """ if as_html: fmt = HTMLExceptionFormatter(limit, with_filenames) else: fmt = TextExceptionFormatter(limit, with_filenames) return fmt.extractStack(f)