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