4

So I have an interesting problem in Python, that could possibly be solved with aspect-oriented techniques. Here's the situation:

  1. I have a bunch of modules, each of which has a bunch of functions.
  2. I have an executable that calls some set of functions in those modules.
  3. When the executable calls one of those functions, I want a log statement generated with the details of the function call (name & parameters)
  4. When one of the modules calls one of the other modules' functions, I do NOT want any log statement generated.

Is there any convenient way to do this in Python, without sticking logging statements inside every one of the modules' functions?

1
  • "Accept" oriented programming ;-) Commented Nov 12, 2009 at 17:18

3 Answers 3

5

Modify the behavior of the functions in the executable can be done using a decorator:

#!/usr/bin/env python
from module1 import foo
from module2 import bar

def trace(f):
    def tracewrapper(*arg, **kw):
        arg_str=','.join(['%r'%a for a in arg]+['%s=%s'%(key,kw[key]) for key in kw])
        print "%s(%s)" % (f.__name__, arg_str)
        return f(*arg, **kw)
    return tracewrapper

verbose_functions=[foo,bar]  # add whatever functions you want logged here
for func in verbose_functions:
    globals()[func.func_name]=trace(func)

Since you are only modifying the definition of the functions in the executable's namespace, the modules' functions remain untouched. When one module's function calls another module's function, it is undecorated by trace and no log statement is generated.

If you would like to log function calls only when they come directly from main(), then you could use a trace decorator like this:

import traceback
def trace(f,filename,funcname):
    def tracewrapper(*arg, **kw):
        stacks=traceback.extract_stack()
        (s_filename,s_lineno,s_funcname,s_text)=stacks[-2]
        # Alternatively, you can search the entire call stack
        # for (s_filename,s_lineno,s_funcname,s_text) in stacks:
        if s_filename.endswith(filename) and s_funcname==funcname: 
            arg_str=','.join(['%r'%a for a in arg]+
                             ['%s=%s'%(key,kw[key]) for key in kw])
            print "%s(%s)" % (f.__name__, arg_str)                
        return f(*arg, **kw)
    return tracewrapper
verbose_functions=[foo,bar]  # add whatever functions you want logged here
for func in verbose_functions:
    # You can pass the module's filename and the function name here
    globals()[func.func_name]=trace(func,'test.py','main')

Note that with the above trace

def baz():
    foo(3,4)
def main():
    foo(1,2,'Hi')
    bar(x=3)
    baz()

would log the foo(1,2,'Hi') and bar(x=3) calls, but not foo(3,4) since this call does not come directly from main. However, it does come indirectly from main, since main calls baz. If you'd like to log the foo(3,4) call, then you'd want to loop through the entire stack:

import traceback
def trace(f,filename,funcname):
    def tracewrapper(*arg, **kw):
        stacks=traceback.extract_stack()        
        for (s_filename,s_lineno,s_funcname,s_text) in stacks:
            if s_filename.endswith(filename) and s_funcname==funcname: 
                arg_str=','.join(['%r'%a for a in arg]+
                                 ['%s=%s'%(key,kw[key]) for key in kw])
                print "%s(%s)" % (f.__name__, arg_str)                
        return f(*arg, **kw)
    return tracewrapper
Sign up to request clarification or add additional context in comments.

2 Comments

Ok, this is pretty close to what I need. However, I only want to print the log message based on information about the caller. How do I get caller information inside the tracewrapper() implementation?
I would like to know the module and function name of the caller; I only want to log the info if the call comes from module 'test', function 'main'.
0

The simplest and first solution that comes to mind, is using a proxy module.

#fooproxy.py
import foo
import logger #not implemented here - use your imagination :)

def bar(baz):
    logger.method("foo.bar")
    return foo.bar(baz)


#foo.py
def bar(baz):
    print "The real McCoy"

#main.py
import fooproxy as foo
foo.bar()

2 Comments

This would require a proxy for every called module that contains a definition for every called function. May not scale very well.
That's certainly workable, but... I have dozens of these modules, and each one of them has maybe as many as 100 functions. So that's a heck of a lot of proxy functions to write (!).
0

I don't know the right way to do this in a general case, but what comes to mind is just hacking the executable a little. Something like:

class DebugCallWrapper(object):
    def __init__(self, callable):
        self.callable = callable

    def __call__(*args,**kwargs):
        log.debug(str(callable) + str(args) + str(kwargs))
        callable(*args,**kwargs)

class Top(object):
    def __getattribute__(self, name):
        real_object = globals()[name]
        if callable(real_object):
            return DebugCallWrapper(real_object)
        else:
            return real_object

top = Top()

import foo
#instead of foo.bar()
top.foo.bar()

This kind of thing could get you into a lot of trouble, and the above probably won't be usable without some tweaking. But maybe it's an idea.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.