@@ -2,13 +2,13 @@
Internal global error types
"""
-import sys, traceback
+import sys, traceback, threading, logging
from traceback import format_exception
# Add names you want to be imported by 'from errors import *' to this list.
# This must be list not a tuple as we modify it to include all of our
# the Exception classes we define below at the end of this file.
-__all__ = ['format_error']
+__all__ = ['format_error', 'context', 'get_context']
def format_error():
@@ -21,6 +21,84 @@ def format_error():
return ''.join(trace)
+# Exception context information:
+# ------------------------------
+# Every function can have some context string associated with it.
+# The context string can be changed by calling context(str) and cleared by
+# calling context() with no parameters.
+# get_context() joins the current context strings of all functions in the
+# provided traceback. The result is a brief description of what the test was
+# doing in the provided traceback (which should be the traceback of a caught
+# exception).
+#
+# For example: assume a() calls b() and b() calls c().
+#
+# def a():
+# error.context("hello")
+# b()
+# error.context("world")
+# get_context() ----> 'world'
+#
+# def b():
+# error.context("foo")
+# c()
+#
+# def c():
+# error.context("bar")
+# get_context() ----> 'hello --> foo --> bar'
+
+context_data = threading.local()
+context_data.contexts = {}
+
+
+def context(c=None, log=logging.info):
+ """
+ Set the context for the currently executing function and log it as an INFO
+ message by default.
+
+ @param c: A string or None. If c is None or an empty string, the context
+ for the current function is cleared.
+ @param log: A logging function to pass the string to. If None, no function
+ will be called.
+ """
+ stack = traceback.extract_stack()
+ try:
+ key_parts = ["%s:%s:%s:" % (filename, func, lineno)
+ for filename, lineno, func, text in stack[:-2]]
+ filename, lineno, func, text = stack[-2]
+ key_parts.append("%s:%s" % (filename, func))
+ key = "".join(key_parts)
+ if c:
+ context_data.contexts[key] = c
+ if log:
+ log("Context: %s" % c)
+ elif key in context_data.contexts:
+ del context_data.contexts[key]
+ finally:
+ # Prevent circular references
+ del stack
+
+
+def get_context(tb):
+ """
+ Construct a context string from the given traceback.
+
+ @param tb: A traceback object (usually sys.exc_info()[2]).
+ """
+ l = []
+ key = ""
+ for filename, lineno, func, text in traceback.extract_tb(tb):
+ key += "%s:%s" % (filename, func)
+ # An exception's traceback is relative to the function that handles it,
+ # so instead of an exact match, we expect the traceback entries to be
+ # suffixes of the stack entries recorded using extract_stack().
+ for key_ in context_data.contexts:
+ if key_.endswith(key):
+ l.append(context_data.contexts[key_])
+ key += ":%s:" % lineno
+ return " --> ".join(l)
+
+
class JobContinue(SystemExit):
"""Allow us to bail out requesting continuance."""
pass