#!/usr/bin/python """ Debugging """ import sys import re import logging import binascii from cStringIO import StringIO # create a root logger root_logger = logging.getLogger('bacpypes') def btox(data, sep=''): """Return the hex encoding of a blob (string).""" # translate the blob into hex hex_str = binascii.hexlify(data) # inject the separator if it was given if sep: hex_str = sep.join(hex_str[i:i+2] for i in range(0, len(hex_str), 2)) # return the result return hex_str def xtob(data, sep=''): """Interpret the hex encoding of a blob (string).""" # remove the non-hex characters data = re.sub("[^0-9a-fA-F]", '', data) # interpret the hex return binascii.unhexlify(data) # # ModuleLogger # def ModuleLogger(globs): """Create a module level logger. To debug a module, create a _debug variable in the module, then use the ModuleLogger function to create a "module level" logger. When a handler is added to this logger or a child of this logger, the _debug variable will be incremented. All of the calls within functions or class methods within the module should first check to see if _debug is set to prevent calls to formatter objects that aren't necessary. """ # make sure that _debug is defined if not globs.has_key('_debug'): raise RuntimeError("define _debug before creating a module logger") # logger name is the module name logger_name = globs['__name__'] # create a logger to be assigned to _log logger = logging.getLogger(logger_name) # put in a reference to the module globals logger.globs = globs # if this is a "root" logger add a default handler for warnings and up if '.' not in logger_name: hdlr = logging.StreamHandler() hdlr.setLevel(logging.WARNING) hdlr.setFormatter(logging.Formatter(logging.BASIC_FORMAT, None)) logger.addHandler(hdlr) return logger # # Typical Use # # some debugging _debug = 0 _log = ModuleLogger(globals()) # # DebugContents # class DebugContents(object): def debug_contents(self, indent=1, file=sys.stdout, _ids=None): """Debug the contents of an object.""" if _debug: _log.debug("debug_contents indent=%r file=%r _ids=%r", indent, file, _ids) klasses = list(self.__class__.__mro__) klasses.reverse() if _debug: _log.debug(" - klasses: %r", klasses) # loop through the classes and look for _debug_contents attrs = [] cids = [] ownFn = [] for klass in klasses: if klass is DebugContents: continue if not issubclass(klass, DebugContents) and hasattr(klass, 'debug_contents'): for i, seenAlready in enumerate(ownFn): if issubclass(klass, seenAlready): del ownFn[i] break ownFn.append(klass) continue # look for a tuple of attribute names if not hasattr(klass, '_debug_contents'): continue debugContents = klass._debug_contents if not isinstance(debugContents, tuple): raise RuntimeError("%s._debug_contents must be a tuple" % (klass.__name__,)) # already seen it? if id(debugContents) in cids: continue cids.append(id(debugContents)) for attr in debugContents: if attr not in attrs: attrs.append(attr) # a bit of debugging if _debug: _log.debug(" - attrs: %r", attrs) _log.debug(" - ownFn: %r", ownFn) # make/extend the list of objects already seen if _ids is None: _ids = [] # loop through the attributes for attr in attrs: # assume you're going deep, but not into lists and dicts goDeep = True goListDict = False goHexed = False # attribute list might want to go deep if attr.endswith("-"): goDeep = False attr = attr[:-1] elif attr.endswith("*"): goHexed = True attr = attr[:-1] elif attr.endswith("+"): goDeep = False goListDict = True attr = attr[:-1] if attr.endswith("+"): goDeep = True attr = attr[:-1] value = getattr(self, attr, None) # skip None if value is None: continue # standard output if goListDict and isinstance(value, list) and value: file.write("%s%s = [\n" % (' ' * indent, attr)) indent += 1 for i, elem in enumerate(value): file.write("%s[%d] %r\n" % (' ' * indent, i, elem)) if goDeep and hasattr(elem, 'debug_contents'): if id(elem) not in _ids: _ids.append(id(elem)) elem.debug_contents(indent + 1, file, _ids) indent -= 1 file.write("%s ]\n" % (' ' * indent,)) elif goListDict and isinstance(value, dict) and value: file.write("%s%s = {\n" % (' ' * indent, attr)) indent += 1 for key, elem in value.items(): file.write("%s%r : %r\n" % (' ' * indent, key, elem)) if goDeep and hasattr(elem, 'debug_contents'): if id(elem) not in _ids: _ids.append(id(elem)) elem.debug_contents(indent + 1, file, _ids) indent -= 1 file.write("%s }\n" % (' ' * indent,)) elif goHexed and isinstance(value, str): if len(value) > 20: hexed = btox(value[:20], '.') + "..." else: hexed = btox(value, '.') file.write("%s%s = x'%s'\n" % (' ' * indent, attr, hexed)) # elif goHexed and isinstance(value, int): # file.write("%s%s = 0x%X\n" % (' ' * indent, attr, value)) else: file.write("%s%s = %r\n" % (' ' * indent, attr, value)) # go nested if it is debugable if goDeep and hasattr(value, 'debug_contents'): if id(value) not in _ids: _ids.append(id(value)) value.debug_contents(indent + 1, file, _ids) # go through the functions ownFn.reverse() for klass in ownFn: klass.debug_contents(self, indent, file, _ids) # # LoggingFormatter # class LoggingFormatter(logging.Formatter): def __init__(self, color=None): logging.Formatter.__init__(self, logging.BASIC_FORMAT, None) # check the color if color is not None: if color not in range(8): raise ValueError("colors are 0 (black) through 7 (white)") # save the color self.color = color def format(self, record): try: # use the basic formatting msg = logging.Formatter.format(self, record) + '\n' # look for detailed arguments for arg in record.args: if isinstance(arg, DebugContents): if msg: sio = StringIO() sio.write(msg) msg = None sio.write(" %r\n" % (arg,)) arg.debug_contents(indent=2, file=sio) # get the message from the StringIO buffer if not msg: msg = sio.getvalue() # trim off the last '\n' msg = msg[:-1] except Exception, err: record_attrs = [ attr + ": " + str(getattr(record, attr, "N/A")) for attr in ('name', 'level', 'pathname', 'lineno', 'msg', 'args', 'exc_info', 'func') ] record_attrs[:0] = ["LoggingFormatter exception: " + str(err)] msg = "\n ".join(record_attrs) if self.color is not None: msg = "\x1b[%dm" % (30+self.color,) + msg + "\x1b[0m" return msg # # bacpypes_debugging # def bacpypes_debugging(obj): """Function for attaching a debugging logger to a class or function.""" # create a logger for this object logger = logging.getLogger(obj.__module__ + '.' + obj.__name__) # make it available to instances obj._logger = logger obj._debug = logger.debug obj._info = logger.info obj._warning = logger.warning obj._error = logger.error obj._exception = logger.exception obj._fatal = logger.fatal return obj # # _LoggingMetaclass # class _LoggingMetaclass(type): def __init__(cls, *args): # wrap the class bacpypes_debugging(cls) # # Logging # class Logging(object): __metaclass__ = _LoggingMetaclass # # class_debugging # def class_debugging(cls): """Add the debugging logger to the class.""" bacpypes_debugging(cls) return cls # # function_debugging # def function_debugging(f): """Add the debugging logger to the function.""" bacpypes_debugging(f) return f