This package provides a decorator for tracing function and method calls in your applications. The tracing capabilities are managed through the logging package, and several mechanisms are provided for controlling the destination of the trace output.
It also provides functionality for adding decorators to existing classes or modules.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 | #! /usr/bin/env python
######################################################################
# Copyright (c) by Kevin L. Sitze on 2011-01-23.
# This code may be used pursuant to the MIT License.
######################################################################
"""This package provides facilities to attach decorators to classes or
modules (possibly recursively). A tracing decorator is provided for
tracing function and method calls in your applications.
from decorators import *
@trace
def function(...):
'''Output goes to <TT>logging.getLogger(__module__)</TT>,
where <TT>__module__</TT> is the name of the module in which
the function is defined. If the current module is '__main__'
then the root logger will be used.
'''
pass
@trace(logger)
def function(...):
'''Output goes to the specified logger instance.
'''
pass
@trace("Logger")
def function(...):
'''Output goes to the named logger instance.
'''
class ClassName(object):
'''Output goes to the ClassName logger. If ClassName is
not in the '__main__' module then the name of its module
will be prefixed to the classname (separated by a dot).
All methods on the class will be traced.
'''
__logger__ = loggerOrLoggerName # optional
__metaclass__ = decorators.TraceMetaClass
def method(...):
pass
class PostDecorate(object):
def method(...):
pass
# attach a decorator to an existing class.
attach(trace(logger), PostDecorate)
You can also attach a decorator to an existing module.
"""
import inspect
import logging
import sys
import thread
import types
from functools import wraps
from itertools import chain
FunctionTypes = tuple(set((
types.BuiltinFunctionType,
types.FunctionType
)))
MethodTypes = tuple(set((
types.BuiltinMethodType,
types.MethodType,
types.UnboundMethodType
)))
class _C(object):
@classmethod
def classMethod(klass): pass
classMethodType = type(classMethod)
@staticmethod
def staticMethod(): pass
staticMethodType = type(staticMethod)
@property
def propertyMethod(self): pass
ClassMethodType = _C.classMethodType
StaticMethodType = _C.staticMethodType
PropertyType = type(_C.propertyMethod)
CallableTypes = tuple(set((
types.BuiltinFunctionType,
types.FunctionType,
types.BuiltinMethodType,
types.MethodType,
types.UnboundMethodType,
ClassMethodType
)))
__all__ = (
'ThreadLocal',
'TraceMetaClass',
'attach',
'getFormatter',
'setFormatter',
'getLoggerFactory',
'setLoggerFactory',
'trace'
)
######################################################################
# Utility functions
######################################################################
MAX_SIZE = 320
def chop(value):
s = repr(value)
if len(s) > MAX_SIZE:
return s[:MAX_SIZE] + '...' + s[-1]
else:
return s
def loggable(obj):
"""Return "True" if the obj implements the minimum Logger API
required by the 'trace' decorator.
"""
if isinstance(obj, logging.getLoggerClass()):
return True
else:
return ( inspect.isclass(obj) and
inspect.ismethod(getattr(obj, 'debug', None)) and
inspect.ismethod(getattr(obj, 'isEnabledFor', None)) and
inspect.ismethod(getattr(obj, 'setLevel', None)) )
######################################################################
# LoggerFactory Property
######################################################################
_logger_factory = logging
def getLoggerFactory():
"""Retrieve the current factory object for creating loggers.
The default is to use the logging module.
"""
global _logger_factory
return _logger_factory
def setLoggerFactory(factory):
"""Set a factory object for creating loggers. This object must
publish a method or class named 'getLogger' that takes a string
parameter naming the logger instance to retrieve. Logger objects
returned by this factory must, at a minimum, expose the methods
'isEnabledFor' and 'debug'.
"""
global _logger_factory
_logger_factory = factory
######################################################################
# class PrependLoggerFactory
######################################################################
class PrependLoggerFactory(object):
"""This is a convenience class for creating new loggers for the
"trace" decorator. All loggers created via this class will have a
user specified prefix prepended to the name of the logger to
instantiate.
"""
def __init__(self, prefix = 'trace'):
"""Construct a new "PrependLoggerFactory" instance that
prepends the value \var{prefix} to the name of each
logger to be created by this class.
"""
self.__prefix = prefix.strip('.')
@property
def prefix(self):
"""The value to prefix to each logger created by this factory.
"""
return self.__prefix
@prefix.setter
def prefix(self, value):
self.__prefix = value.strip('.')
def getLogger(self, name):
return logging.getLogger('.'.join((self.__prefix, name)))
######################################################################
# class ThreadLocal
######################################################################
class ThreadLocal(object):
"""Instances of this class provide a thread-local variable.
"""
def __init__(self):
self.__lock = thread.allocate_lock()
self.__vars = dict()
self.__init = None
@property
def value(self):
with self.__lock:
try:
return self.__vars[thread.get_ident()]
except KeyError:
return self.__init
@value.setter
def value(self, value):
with self.__lock:
self.__vars[thread.get_ident()] = value
@property
def initialValue(self):
with self.__lock:
return self.__init
@initialValue.setter
def initialValue(self, value):
with self.__lock:
self.__init = value
######################################################################
# Formatter functions
######################################################################
def _formatter_self(name, value):
"""Format the "self" variable and value on instance methods.
"""
__mname = value.__module__
if __mname != '__main__':
return '%s = <%s.%s object at 0x%x>' % (name, __mname, value.__class__.__name__, id(value))
else:
return '%s = <%s object at 0x%x>' % (name, value.__class__.__name__, id(value))
def _formatter_class(name, value):
"""Format the "klass" variable and value on class methods.
"""
__mname = value.__module__
if __mname != '__main__':
return "%s = <type '%s.%s'>" % (name, __mname, value.__name__)
else:
return "%s = <type '%s'>" % (name, value.__name__)
def _formatter_named(name, value):
"""Format a named parameter and its value.
"""
return '%s = %s' % (name, chop(value))
def _formatter_defaults(name, value):
return '[%s = %s]' % (name, chop(value))
af_self = _formatter_self
af_class = _formatter_class
af_named = _formatter_named
af_default = _formatter_defaults
af_unnamed = chop
af_keyword = _formatter_named
def getFormatter(name):
"""Return the named formatter function. See the function
"setFormatter" for details.
"""
if name in ( 'self', 'instance', 'this' ):
return af_self
elif name == 'class':
return af_class
elif name in ( 'named', 'param', 'parameter' ):
return af_named
elif name in ( 'default', 'optional' ):
return af_default
elif name in ( 'anonymous', 'arbitrary', 'unnamed' ):
return af_anonymous
elif name in ( 'keyword', 'pair', 'pairs' ):
return af_keyword
else:
raise ValueError('unknown trace formatter %r' % name)
def setFormatter(name, func):
"""Replace the formatter function used by the trace decorator to
handle formatting a specific kind of argument. There are several
kinds of arguments that trace discriminates between:
* instance argument - the object bound to an instance method.
* class argument - the class object bound to a class method.
* positional arguments (named) - values bound to distinct names.
* positional arguments (default) - named positional arguments with
default values specified in the function declaration.
* positional arguments (anonymous) - an arbitrary number of values
that are all bound to the '*' variable.
* keyword arguments - zero or more name-value pairs that are
placed in a dictionary and bound to the double-star variable.
\var{name} - specifies the name of the formatter to be modified.
* instance argument - "self", "instance" or "this"
* class argument - "class"
* named argument - "named", "param" or "parameter"
* default argument - "default", "optional"
* anonymous argument - "anonymous", "arbitrary" or "unnamed"
* keyword argument - "keyword", "pair" or "pairs"
\var{func} - a function to format an argument.
* For all but anonymous formatters this function must accept two
arguments: the variable name and the value to which it is bound.
* The anonymous formatter function is passed only one argument
corresponding to an anonymous value.
* if \var{func} is "None" then the default formatter will be used.
"""
if name in ( 'self', 'instance', 'this' ):
global af_self
af_self = _formatter_self if func is None else func
elif name == 'class':
global af_class
af_class = _formatter_class if func is None else func
elif name in ( 'named', 'param', 'parameter' ):
global af_named
af_named = _formatter_named if func is None else func
elif name in ( 'default', 'optional' ):
global af_default
af_default = _formatter_defaults if func is None else func
elif name in ( 'anonymous', 'arbitrary', 'unnamed' ):
global af_anonymous
af_anonymous = chop if func is None else func
elif name in ( 'keyword', 'pair', 'pairs' ):
global af_keyword
af_keyword = _formatter_named if func is None else func
else:
raise ValueError('unknown trace formatter %r' % name)
######################################################################
# Decorator: trace
######################################################################
__builtins = (
'__import__(name,globals={},locals={},fromlist=[],level=-1)',
'abs(number)',
'all(iterable)',
'any(iterable)',
'apply(object,args=[],kwargs={})',
'bin(number)',
'callable(object)',
'chr(i)',
'cmp(x,y)',
'coerce(x,y)',
'compile(source,filename,mode,flags=0,dont_inherit=0)',
'delattr(object,name)',
'dir()',
'divmod(x,y)',
'eval(source,globals={},locals={})',
'execfile(filename,globals={},locals={})',
'filter(function,sequence)',
'format(value,format_spec="")',
'getattr(object,name)',
'globals()',
'hasattr(object,name)',
'hash(object)',
'hex(number)',
'id(object)',
'input(prompt=None)',
'intern(string)',
'isinstance(object,klass)',
'issubclass(C,B)',
'iter(collection)',
'len(object)',
'locals()',
'map(function,sequence)',
'max(iterable,key=None)',
'min(iterable,key=None)',
'next(iterator)',
'oct(number)',
'open(name,mode=0666,buffering=True)',
'ord(c)',
'pow(x,y,z=None)',
'range()',
'raw_input(prompt=None)',
'reduce(function,sequence)',
'reload(module)',
'repr(object)',
'round(number,ndigits=0)',
'setattr(object,name,value)',
'sorted(iterable,cmp=None,key=None,reverse=False)',
'sum(sequence,start=0)',
'unichr(i)',
'vars()',
'zip(sequence)'
)
__builtin_defaults = {
'""': "",
'-1': -1,
'0' : 0,
'0666': 0666,
'False': False,
'None': None,
'True': True,
'[]': list(),
'{}': dict()
}
__builtin_functions = None
def __lookup_builtin(name):
"""Lookup the parameter name and default parameter values for
builtin functions.
"""
global __builtin_functions
if __builtin_functions is None:
builtins = dict()
for proto in __builtins:
pos = proto.find('(')
name, params, defaults = proto[:pos], list(), dict()
for param in proto[pos+1:-1].split(','):
pos = param.find('=')
if not pos < 0:
param, value = param[:pos], param[pos+1:]
try:
defaults[param] = __builtin_defaults[value]
except KeyError:
raise ValueError( 'builtin function %s: parameter %s: unknown default %r' % ( name, param, value ) )
params.append(param)
builtins[name] = ( params, defaults )
__builtin_functions = builtins
try:
params, defaults = __builtin_functions[name]
except KeyError:
params, defaults = tuple(), dict()
__builtin_functions[name] = ( params, defaults )
print >>sys.stderr, "Warning: builtin function %r is missing prototype" % name
return ( len(params), params, defaults )
_ = ThreadLocal()
_.initialValue = False
def trace(_name):
"""Function decorator that logs function entry and exit details.
\var{_name} a string, an instance of logging.Logger or a function.
Construct a function or method proxy to generate call traces.
"""
def decorator(_func):
"""This is the actual decorator function that wraps the
\var{_func} function for detailed logging.
"""
def positional(name, value):
"""Format one named positional argument.
"""
if name is __self:
return af_self(name, value)
elif name is __klass:
return af_class(name, value)
else:
return af_named(name, value)
def wrapper(*__argv, **__kwds):
if not logger.isEnabledFor(logging.DEBUG) or _.value:
return _func(*__argv, **__kwds)
try:
_.value = True
params = dict(co_defaults)
params.update(__kwds)
params.update(zip(co_varnames, __argv))
position = [ positional(n, params.pop(n)) for n in co_varnames[:len(__argv)] ]
defaults = [ af_default(n, params.pop(n)) for n in co_varnames[len(__argv):] ]
nameless = ( af_unnamed(v) for v in __argv[co_argcount:] )
keywords = ( af_keyword(n, params[n]) for n in sorted(params.keys()) )
params = ', '.join(filter(None, chain(position, defaults, nameless, keywords)))
# params = params.replace(', [', '[, ').replace('][, ', ', ')
enter = [ pre_enter ]
if params:
enter.append(' ')
enter.append(params)
enter.append(' ')
enter.append(')')
leave = [ pre_leave ]
try:
logger.debug(''.join(enter))
try:
try:
_.value = False
result = _func(*__argv, **__kwds)
finally:
_.value = True
except:
type, value, traceback = sys.exc_info()
leave.append(' => exception thrown\n\traise ')
__mname = type.__module__
if __mname != '__main__':
leave.append(__mname)
leave.append('.')
leave.append(type.__name__)
if value.args:
leave.append('(')
leave.append(', '.join(chop(v) for v in value.args))
leave.append(')')
else:
leave.append('()')
raise
else:
if result is not None:
leave.append(' => ')
leave.append(chop(result))
finally:
logger.debug(''.join(leave))
finally:
_.value = False
return result
####
# decorator
####
__self = False
__klass = False
__rewrap = lambda x: x
if type(_func) in FunctionTypes:
# functions do not belong to a class.
__cname = None
elif type(_func) in MethodTypes:
# im_self is None for unbound instance methods.
# Assumption: trace is only called on unbound methods.
if _func.im_self is not None:
__rewrap = classmethod
__cname = _func.im_self.__name__
__klass = True
else:
__cname = _func.im_class.__name__
__self = True
_func = _func.im_func
else:
# other callables are not supported yet.
return _func
__module = _func.__module__
__fname = _func.__name__
# Do not wrap initialization and conversion methods.
if __fname in ('__init__', '__new__', '__repr__', '__str__'):
return __rewrap(_func)
# Generate the Fully Qualified Function Name.
__fqfn = list()
if __module != '__main__':
__fqfn.append(__module)
__fqfn.append('.')
if __cname is not None:
__fqfn.append(__cname)
__fqfn.append('.')
__fqcn = ''.join(__fqfn)
__fqfn.append(__fname)
__fqfn = ''.join(__fqfn)
if type(_name) in CallableTypes:
logger = getLoggerFactory().getLogger(__fqfn)
elif loggable(_name):
logger = _name
elif isinstance(_name, basestring):
logger = getLoggerFactory().getLogger(_name)
else:
raise ValueError('invalid object %r: must be a function, a method, a string or an object that implements the Logger API' % _name)
pre_enter = [ '>>> ' ]
pre_enter.append(__fqfn)
pre_enter.append('(')
pre_enter = ''.join(pre_enter)
pre_leave = [ '<<< ' ]
pre_leave.append(__fqfn)
pre_leave = ''.join(pre_leave)
####
# Here we are really mucking around in function internals.
# func_code is the low level 'code' instance that describes
# the function arguments, variable and other stuff.
#
# func.func_code.co_argcount - number of function arguments.
# func.func_code.co_varnames - function variables names, the
# first co_argcount values are the argument names.
# func.func_defaults - contains default arguments
try:
code = _func.func_code
except AttributeError:
co_argcount , \
co_varnames , \
co_defaults = __lookup_builtin(_func.__name__)
else:
co_argcount = code.co_argcount
co_varnames = code.co_varnames[:co_argcount]
if _func.func_defaults:
co_defaults = dict(zip(co_varnames[-len(_func.func_defaults):], _func.func_defaults))
else:
co_defaults = dict()
if __klass:
__klass = co_varnames[0]
if __self:
__self = co_varnames[0]
return __rewrap(wraps(_func)(wrapper))
####
# trace
####
logging.basicConfig(level = logging.DEBUG)
if type(_name) in CallableTypes:
return decorator(_name)
else:
return decorator
######################################################################
# attach: apply decorator to a class or module
######################################################################
def attachToProperty(decorator, klass, k, prop_attr, prop_decorator):
if prop_attr is not None:
setattr(klass, k, prop_attr)
value = decorator(getattr(klass, k))
else:
value = None
# Passing "None" to the property decorator causes the new property
# to assume the original value of the associated attribute.
return prop_decorator(value)
def attachToClass(decorator, klass, recursive = True):
for k, v in klass.__dict__.iteritems():
t = type(v)
if t is types.FunctionType or t is ClassMethodType:
setattr(klass, k, decorator(getattr(klass, k)))
elif t is StaticMethodType:
setattr(klass, k, staticmethod(decorator(getattr(klass, k))))
elif t is PropertyType:
value = getattr(klass, k)
value = attachToProperty(decorator, klass, k, value.fget, value.getter)
value = attachToProperty(decorator, klass, k, value.fset, value.setter)
value = attachToProperty(decorator, klass, k, value.fdel, value.deleter)
setattr(klass, k, value)
elif recursive and inspect.isclass(v):
attachToClass(decorator, v, recursive)
def attach(decorator, obj, recursive = True):
"""attach(decorator, class_or_module[, recursive = True])
Utility to attach a \val{decorator} to the \val{obj} instance.
If \val{obj} is a module, the decorator will be attached to every
function and class in the module.
If \val{obj} is a class, the decorator will be attached to every
method and subclass of the class.
if \val{recursive} is "True" then subclasses will be decorated.
"""
if inspect.ismodule(obj):
for name, fn in inspect.getmembers(obj, inspect.isfunction):
setattr(obj, name, decorator(fn))
for name, klass in inspect.getmembers(obj, inspect.isclass):
attachToClass(decorator, klass, recursive)
elif inspect.isclass(obj):
attachToClass(decorator, obj, recursive)
######################################################################
# class TraceMetaClass
######################################################################
class TraceMetaClass(type):
"""Metaclass to automatically attach the 'trace' decorator to all
methods, static method and class methods of the class.
"""
def __new__(meta, className, bases, classDict):
klass = super(TraceMetaClass, meta).__new__(meta, className, bases, classDict)
if classDict.has_key('__logger__'):
hook = trace(classDict['__logger__'])
else:
hook = trace
attachToClass(hook, klass, False)
return klass
if __name__ == '__main__':
logging.basicConfig(level = logging.DEBUG)
logger = logging.root
class Test(object):
__logger__ = logger
__metaclass__ = TraceMetaClass
@classmethod
def classMethod(klass):
pass
@staticmethod
def staticMethod():
pass
__test = None
@property
def test(self):
return self.__test
@test.setter
def test(self, value):
self.__test = value
def method(self):
pass
Test.classMethod()
Test.staticMethod()
test = Test()
test.test = 1
assert 1 == test.test
test.method()
class Test(object):
@classmethod
def classMethod(klass):
pass
@staticmethod
def staticMethod():
pass
__test = None
@property
def test(self):
return self.__test
@test.setter
def test(self, value):
self.__test = value
def method(self):
pass
def __str__(self):
return 'Test(' + str(self.test) + ')'
attach(trace(logger), Test)
Test.classMethod()
Test.staticMethod()
test = Test()
test.test = 1
assert 1 == test.test
test.method()
print str(test)
@trace(logger)
def test(x, y, z = True):
a = x + y
b = x * y
if z: return a
else: return b
test(5, 5)
test(5, 5, False)
setLoggerFactory(PrependLoggerFactory())
@trace('main')
def test(x, *argv, **kwds):
"""Simple test
"""
return x + sum(argv)
test(5)
test(5, 5)
test(5, 5, False)
test(5, 5, False, name = 10)
test( *xrange(50) )
assert test.__doc__ == 'Simple test\n '
assert test.__name__ == 'test'
myzip = trace('main')(zip)
for i, j in myzip(xrange(5), xrange(5, 10)):
print i, j
|
This package provides facilities to attach decorators to classes or modules (possibly recursively). A tracing decorator is provided for tracing function and method calls in your applications.
To use this module, save the code into a file named "decorators.py" in your python library path. Add one of the following import statements to your code.
from decorators import *
from decorators import trace
To trace a single function call add the "@trace" decorator to the function.
@trace
def function(...):
pass
This trace decorator will send a log message to the debug log stream when function
is called and when it returns (even if that return is a result of an exception). The logger itself is created using a logger factory (see decorators.getLoggerFactory
in source code). The name of the logger is the concatenation of the module name and the function name, unless the module is '__main__' in which case only the function name is used.
You can also send the output of the trace decorator to a specific logger. This logger must be either a subclass of logging.Logger
or it may be an instance of your own log class. To use your own class, you must expose the isEnabledFor
and debug
methods as published by the logging.Logger
class.
@trace(logger)
def function(...):
pass
You may optionally name the logger to use by passing the trace decorator a string instead of a logging object. This string will be passed unchanged to the getLogger
method of the current LoggerFactory
object (again, see decorators.getLoggerFactory
).
@trace("Logger")
def function(...):
pass
You can trace all methods on a class too. If the class is one your application owns, simply change its metaclass to decorators.TraceMetaClass. All methods on the class will automatically have the trace decorator applied.
class ClassName(object):
__logger__ = loggerOrLoggerName # optional
__metaclass__ = decorators.TraceMetaClass
def method( self, ...):
pass
Instance and class methods are handled specially by the trace decorator in that it understands the protocol that the first parameter is actually the instance or class object so that it can adjust the logging output slightly for these cases.
Note also the special __logger__
class variable that allows you to specify the logger name or object for this class.
If you need to trace a 3rd party class or module, you can attach
the trace decorator to that class or module.
from 3rdPartyModule import AClass
attach(trace(my_logger_or_logger_name), AClass)
You can also attach a decorator to an existing module.
import 3rdPartyModule
attach(trace, 3rdPartyModule)
Notes and Bugs:
The package attempts to avoid infinite recursion scenarios by distinguishing between running application code and trace code. When trace code is executing and the trace decorator is entered the execution flow of the decorator will be short-circuited to the decorated function. To make this entry and exit detection thread safe I write a simple ThreadLocal class to track this information separately for different threads. ThreadLocal may be useful all on its own in your own threaded code (despite not automatically GC'ing values for threads that have exited).
While the __str__
and __repr__
methods on classes are never decorated by trace (even if explicitly decorated) infinite recursion through such methods can still occur. The __new__
and __init__
methods are likewise suppressed because attempting to log information about a class prior to its construction rarely leads to runnable code.
Tracing is inherently dangerous. If you need to trace 3rd party code the author of this module strongly suggests that you do so with a least-impact philosophy. Only decorate the specific function you need to observe. Don't decorate an entire class if a single method will do, don't decorate an entire module if a single class will do.
I've added code to "sort of handle" builtin (C-level) functions. This list is by no means comprehensive. If your application attempts to trace builtin functions found in Python packages you'll get a warning message for each function other than those already found in the __builtins__ module.
While I've made some effort at making sure that functions decorated with the trace decorator will remain thread safe if they were originally thread safe, the process of actually applying the trace decoration to functions is not an inherently thread safe process. What this means is that you should spawn threads in your application AFTER applying the trace decoration to those modules, classes and functions you wish to observe.
If you use logging.basicConfig
to configure logging for your application, you are strongly encouraged to do this before using the trace decorator. This is because the decorator calls logging.basicConfig
. This is likely a bug. It should delegate this to the factory (but the factory defaults to the logging module). In any case you can do this in your application code properly as follows:
# BEFORE importing any other module do this
import logging
logging.basicConfig(...)
import my_own_modules_here
If you want to send all trace output to a single logger, do this in your application
import logging
import decorators
trace = decorators.trace(logging.getLogger(my_logger_name))
@trace
def function(...): pass
By creating the trace decorator with a logger up front, it may make it easier for you to control your logging output. Another mechanism for centralizing control of the trace output is to construct a PrependLoggerFactory that adds a known value and a dot to the front of the name of each logger created by the trace descriptor. This allows your application to control all logging at a high level, as well as fine tuned control of logging over individual modules, classes or functions.
The name of this module is 'decorators' when 'observe' is probably a better name.
Changes:
This version has the formatting of various argument kinds broken out into separate functions. These functions may be changed under application control so that arguments in the trace output may be customized to increase or decrease the overall information level.
The <TT> element is supposed to convert its contents to monospaced type. The Markdown syntax for doing this kind of thing is not obvious and I couldn't find it in a quick review of the documentation at http://daringfireball.net/projects/markdown/syntax. This sucks and I'm sorry.
This is the result of running the above code on my machine:
Kevin, the markdown syntax for inline code is the backtick (`). It works like underscores/asterisks for emphasis (i.e. place one on each side). It's the same character you use for inline execution in shell commands or database/table/field names in MySQL.