Last active
May 7, 2024 18:25
-
-
Save kellyjonbrazil/6dfe6ed8a3ea3f785545c5bc59495501 to your computer and use it in GitHub Desktop.
Python Tracebackplus
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""More comprehensive traceback formatting for Python scripts. | |
To enable this module, do: | |
import tracebackplus; tracebackplus.enable() | |
at the top of your script. The optional arguments to enable() are: | |
logdir - if set, tracebacks are written to files in this directory | |
context - number of lines of source code to show for each stack frame | |
By default, tracebacks are displayed but not saved and the context is 5 lines. | |
Alternatively, if you have caught an exception and want tracebackplus to display it | |
for you, call tracebackplus.handler(). The optional argument to handler() is a | |
3-item tuple (etype, evalue, etb) just like the value of sys.exc_info(). | |
""" | |
''' | |
Licensing: | |
MIT License | |
Copyright (c) 2020 Kelly Brazil | |
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
tracebackplus was derived from the cgitb standard library module. As cgitb is being | |
deprecated, this simplified version of cgitb was created. | |
https://github.com/python/cpython/blob/3.8/Lib/cgitb.py | |
"Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, | |
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; | |
All Rights Reserved" | |
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 | |
-------------------------------------------- | |
1. This LICENSE AGREEMENT is between the Python Software Foundation | |
("PSF"), and the Individual or Organization ("Licensee") accessing and | |
otherwise using this software ("Python") in source or binary form and | |
its associated documentation. | |
2. Subject to the terms and conditions of this License Agreement, PSF hereby | |
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, | |
analyze, test, perform and/or display publicly, prepare derivative works, | |
distribute, and otherwise use Python alone or in any derivative version, | |
provided, however, that PSF's License Agreement and PSF's notice of copyright, | |
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, | |
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; | |
All Rights Reserved" are retained in Python alone or in any derivative version | |
prepared by Licensee. | |
3. In the event Licensee prepares a derivative work that is based on | |
or incorporates Python or any part thereof, and wants to make | |
the derivative work available to others as provided herein, then | |
Licensee hereby agrees to include in any such work a brief summary of | |
the changes made to Python. | |
4. PSF is making Python available to Licensee on an "AS IS" | |
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR | |
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND | |
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS | |
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT | |
INFRINGE ANY THIRD PARTY RIGHTS. | |
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON | |
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS | |
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, | |
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. | |
6. This License Agreement will automatically terminate upon a material | |
breach of its terms and conditions. | |
7. Nothing in this License Agreement shall be deemed to create any | |
relationship of agency, partnership, or joint venture between PSF and | |
Licensee. This License Agreement does not grant permission to use PSF | |
trademarks or trade name in a trademark sense to endorse or promote | |
products or services of Licensee, or any third party. | |
8. By copying, installing or otherwise using Python, Licensee | |
agrees to be bound by the terms and conditions of this License | |
Agreement. | |
''' | |
import inspect | |
import keyword | |
import linecache | |
import os | |
import pydoc | |
import sys | |
import tempfile | |
import time | |
import tokenize | |
import traceback | |
__UNDEF__ = [] # a special sentinel object | |
def lookup(name, frame, locals): | |
"""Find the value for a given name in the given environment.""" | |
if name in locals: | |
return 'local', locals[name] | |
if name in frame.f_globals: | |
return 'global', frame.f_globals[name] | |
if '__builtins__' in frame.f_globals: | |
builtins = frame.f_globals['__builtins__'] | |
if isinstance(builtins, dict): | |
if name in builtins: | |
return 'builtin', builtins[name] | |
else: | |
if hasattr(builtins, name): | |
return 'builtin', getattr(builtins, name) | |
return None, __UNDEF__ | |
def scanvars(reader, frame, locals): | |
"""Scan one logical line of Python and look up values of variables used.""" | |
vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__ | |
for ttype, token, start, end, line in tokenize.generate_tokens(reader): | |
if ttype == tokenize.NEWLINE: | |
break | |
if ttype == tokenize.NAME and token not in keyword.kwlist: | |
if lasttoken == '.': | |
if parent is not __UNDEF__: | |
value = getattr(parent, token, __UNDEF__) | |
vars.append((prefix + token, prefix, value)) | |
else: | |
where, value = lookup(token, frame, locals) | |
vars.append((token, where, value)) | |
elif token == '.': | |
prefix += lasttoken + '.' | |
parent = value | |
else: | |
parent, prefix = None, '' | |
lasttoken = token | |
return vars | |
def text(einfo, context=5): | |
"""Return a plain text document describing a given traceback.""" | |
etype, evalue, etb = einfo | |
if isinstance(etype, type): | |
etype = etype.__name__ | |
pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable | |
date = time.ctime(time.time()) | |
head = '%s\n%s\n%s\n' % (str(etype), pyver, date) + ''' | |
A problem occurred in a Python script. Here is the sequence of | |
function calls leading up to the error, in the order they occurred. | |
''' | |
frames = [] | |
records = inspect.getinnerframes(etb, context) | |
for frame, file, lnum, func, lines, index in records: | |
file = file and os.path.abspath(file) or '?' | |
args, varargs, varkw, locals = inspect.getargvalues(frame) | |
call = '' | |
if func != '?': | |
call = 'in ' + func + \ | |
inspect.formatargvalues(args, varargs, varkw, locals, | |
formatvalue=lambda value: '=' + pydoc.text.repr(value)) | |
highlight = {} | |
def reader(lnum=[lnum]): | |
highlight[lnum[0]] = 1 | |
try: | |
return linecache.getline(file, lnum[0]) | |
finally: | |
lnum[0] += 1 | |
vars = scanvars(reader, frame, locals) | |
rows = [' %s %s' % (file, call)] | |
if index is not None: | |
i = lnum - index | |
for line in lines: | |
num = '%5d ' % i | |
rows.append(num + line.rstrip()) | |
i += 1 | |
done, dump = {}, [] | |
for name, where, value in vars: | |
if name in done: | |
continue | |
done[name] = 1 | |
if value is not __UNDEF__: | |
if where == 'global': | |
name = 'global ' + name | |
elif where != 'local': | |
name = where + name.split('.')[-1] | |
dump.append('%s = %s' % (name, pydoc.text.repr(value))) | |
else: | |
dump.append(name + ' undefined') | |
rows.append('\n'.join(dump)) | |
frames.append('\n%s\n' % '\n'.join(rows)) | |
exception = ['%s: %s' % (str(etype), str(evalue))] | |
for name in dir(evalue): | |
value = pydoc.text.repr(getattr(evalue, name)) | |
exception.append('\n%s%s = %s' % (' ' * 4, name, value)) | |
return head + ''.join(frames) + ''.join(exception) + ''' | |
The above is a description of an error in a Python program. Here is | |
the original traceback: | |
%s | |
''' % ''.join(traceback.format_exception(etype, evalue, etb)) | |
class Hook: | |
"""A hook to replace sys.excepthook""" | |
def __init__(self, logdir=None, context=5, file=None): | |
self.logdir = logdir # log tracebacks to files if not None | |
self.context = context # number of source code lines per frame | |
self.file = file or sys.stdout # place to send the output | |
def __call__(self, etype, evalue, etb): | |
self.handle((etype, evalue, etb)) | |
def handle(self, info=None): | |
info = info or sys.exc_info() | |
formatter = text | |
try: | |
doc = formatter(info, self.context) | |
except: # just in case something goes wrong | |
doc = ''.join(traceback.format_exception(*info)) | |
self.file.write(doc + '\n') | |
if self.logdir is not None: | |
suffix = '.txt' | |
(fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir) | |
try: | |
with os.fdopen(fd, 'w') as file: | |
file.write(doc) | |
msg = '%s contains the description of this error.' % path | |
except: | |
msg = 'Tried to save traceback to %s, but failed.' % path | |
self.file.write(msg + '\n') | |
try: | |
self.file.flush() | |
except: | |
pass | |
handler = Hook().handle | |
def enable(logdir=None, context=5): | |
"""Install an exception handler that sends verbose tracebacks to STDOUT.""" | |
sys.excepthook = Hook(logdir=logdir, context=context) |
My bad, ta!
ps. Looking at the original cgitb code -- apparently it is indeed not too hard to self-maintain: in fact, there was a tiny little change between v.3.8 and 3.11, after what apparently python devs got exhausted and decided that that would be too much for the day
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi there - it looks like you are initializing tracebackplus with a single, unnamed argumet (
5
).Here is the init definition:
I would use
tracebackplus.enable(context=5)
instead oftracebackplus.enable(5)
as it is mistakingcontext
forlogdir
.