Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: Force decorator inheritance?

I'm working with quite a large OOP code-base, and I'd like to inject some tracing/logging. The easiest way to do this would be to introduce a decorator around certain methods on some base classes, but unfortunately decorators aren't inherited.

I did try something like the following:

def trace(fn):
    def wrapper(instance, *args, **kwargs):
        result = fn(instance, *args, **kwargs)
        # trace logic...
        return result
    return wrapper

class BaseClass(object):
    def __init__(self, ...):
        ...
        self.__call__ = trace(self.__call__)  # line added to end of method

...and while the __call__ method is wrapped (which I can see from printing the instance information to the console) the wrapper function isn't executed as expected.

I also looked briefly at using a metaclass based on this answer, but it instantly breaks other parts of the system that use introspection, so I think that's a no-go.

Is there any other way I can force the application of these decorators around the __call__ method of classes that inherit from BaseClass?

like image 845
Phillip B Oldham Avatar asked Jan 14 '14 14:01

Phillip B Oldham


2 Answers

Why would metaprogramming mess up introspection? Perhaps you are not using it correctly? Try this (assuming Python2.x):

class MyMeta(type):
    def __new__(mcl, name, bases, nmspc):
        if "__call__" in nmspc:
            nmspc["__call__"] = trace(nmspc["__call__"])
        return super(MyMeta, mcl).__new__(mcl, name, bases, nmspc)

class BaseClass(object):
    __metaclass__ = MyMeta

You can then simply inherit from BaseClass and __call__ will get wrapped automatically.

I'm not sure what kind of introspection would break that. Unless BaseClass actually does not inherit from object but from something that implements its own meta?? But you can deal with that case as well by forcing MyMeta to inherit from that parent's meta (instead of type).

like image 94
freakish Avatar answered Nov 11 '22 23:11

freakish


You can't do that... see things like here: Python method resolution mystery

Basically. most of special functions are looked up at the class level. Monkey-patching it after initialization won't have any effect.

That said, you should be able to do something like this:

class BaseClass(object):
    __call__ = trace(object.__call__)
    def __init__(self, ...):
        ...

Now, it's wrapped at the class level, so it will work.

edit: That's not really an answer. What you want is something like:

class BaseClass(object):
    @decorator
    __call__ = something

class SubClass(BaseClass):
    __call__ = something_else

In this case... it doesn't matter what you put for the BaseClass __call__ method, the SubClass method will override it. It's not about decorators vs. non-decorators, as it's functions (well, really, attributes, since there isn't really much difference in python) that get inherited. Decorators look separate from the function, but they aren't really - in the above, all that the decorator does is change the definition of __call__ from something to decorator(something) (which must still return a callable).

Then, in the subclass, you reassign this function to something completely different.

You could use a class decorator, but those things tend to break introspection as you noticed; they turn a class, into a function that returns a class. Similar problem with factory functions.

Maybe look into the profile module? I know there are some lower-level hooks that it uses to track function calls etc. that you might be able to use as well (though don't know much about them).

like image 34
Corley Brigman Avatar answered Nov 11 '22 22:11

Corley Brigman