Simple Error Reporter

A simple way to allow users to report errors on scripts and tools. To use, wrap your code in the Report() class. Either as a Decorator:

@Report()
def myfunc():
pass

or as a context manager:

with Report():
pass

Wrap the outermost code that will run and any errors passing through will prompt the user to email a brief report. The report is brief (would prefer to use cgitb) as there is a small finite limit to “mailto:” urls.

For general purpose, easy, small time reporting it does its job. :)

# Simple Error report mechanism squeezing into mailto: character limit
# Created By Jason Dixon. http://internetimagery.com
#
# Wrap the outermost function calls in the Report class
# As a decorator or as a context manager on the outermost function calls
# For instance, decorate your Main() function,
# or any function that is called directly by a GUI
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

### Change these variables to suit ###

CONTACT = "myemail@provider.com" # Who will be emailed?
SUBJECT = "Error Report for SCRIPTNAME" # Subject of the email
CONFIRM_MSG = """
OH NO!
There was a problem and a brief error report has been created.
Would you like to send it?
"""
# Message in confirm dialog
OVERSIZE_MSG = "Report Continues..." # Message if report is too long and cut off


### Don't change things beyond this point ###

import re
import urllib
import inspect
import datetime
import platform
import functools
import webbrowser

class Report(object):
""" Report Errors """
depth = 0 # Follow report depth
def __init__(s, char_limit=2083):
s.char_limit = char_limit # Max characters for mailto: (0 = no limit)

def __call__(s, func):
""" Decorate a function. Capture and report any errors """
@functools.wraps(func)
def inner(*args, **kwargs):
with s:
return func(*args, **kwargs)
return inner

def __enter__(s): Report.depth += 1

def __exit__(s, eType, eVal, eTrace):
""" Report Errors if they happened """
Report.depth -= 1
if eType and not Report.depth: # We have an error?
if s.consent(eType, eVal):
text = [
str(datetime.datetime.now()),
platform.platform(),
"%s: %s" % (eType.__name__, eVal),
s.software()
]
text += list(s.compact_trace(eTrace))

url = "mailto:%s?subject=%s&body=%s" % (
CONTACT,
urllib.quote(SUBJECT),
urllib.quote("\n".join(text))
)
if s.char_limit and s.char_limit < len(url): # We've gone too big. Note that at the bottom
note = urllib.quote("\n\n%s" % OVERSIZE_MSG)
url = url[:s.char_limit - len(note)] + note

webbrowser.open(url) # Open email!

def consent(s, eType, eVal):
""" Ask user to consent to send message """
try:
import maya.cmds as cmds # Is Maya active? Ask using their GUI
answer = cmds.confirmDialog(t=eType.__name__, m=CONFIRM_MSG, b=("Yes","No"), db="Yes", cb="No", ds="No")
return "Yes" == answer
except ImportError:
return True # No means to ask? Ah well ...

def software(s):
""" Return information about software """
try:
import maya.mel as mel
version = mel.eval("$tmp = getApplicationVersionAsFloat();")
return "Maya, %s" % version
except ImportError:
pass
return "Unknown software."

def compact_trace(s, trace):
""" Format traceback compactly """
filepath = None
for frame, path, line, func, context, i in reversed(inspect.trace()):
code = context[i].strip()

# Tell us which file and function we are in!
if filepath == path: # Skip repeating filename
yield "In \"%s\":" % func
else:
filepath = path
yield "In \"%s\" %s:" % (func, path)

# Tell us the line number and code
yield "<%s> %s" % (line, code)

# Tell us the value of relevant variables (attributes using dot notation)
all_vars = dict(frame.f_globals, **frame.f_locals)
tokens = set(re.split(r"[^\w\.]+", code))
tokens |= set(b for a in tokens for b in a.split(".")) # Add in partial names
for a, b in all_vars.iteritems():
for var, val in s.collect_vars(tokens, a, b):
if var != func:
yield "%s=%s" % (var, repr(val))

def collect_vars(s, code, var, val):
""" Collect relevant variables """
if var in code:
yield var, val
for attr in dir(val):
for a in s.collect_vars(code, ".".join((var, attr)), getattr(val, attr)):
yield a

if __name__ == '__main__':
# Some example usage!
@Report() # As Decorator
def recurse(num):
# Something
1 / num
recurse(num -1)
with Report(): # As Context manager
recurse(5)
rope_end