Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: Logging all of a class' methods without decorating each one

I want to log every method call in some classes. I could have done

class Class1(object):
    @log
    def method1(self, *args):
        ...
    @log
    def method2(self, *args):
        ...

But I have a lot of methods in every class, and I don't want to decorate every one separately. Currently, I tried using a hack with metaclasses (overriding my logged class' __getattribute__ so that if I try to get a method, it'll return a logging method instead):

class LoggedMeta(type):
    def __new__(cls, name, bases, attrs):
        def __getattribute__(self, name_):
            attr = super().__getattribute__(name_)
            if isinstance(attr, (types.MethodType, types.FunctionType)) and not name_.startswith("__"):
                return makeLogged(attr) #This returns a method that first logs the method call, and then calls the original method.
            return attr
        attrs["__getattribute__"] = __getattribute__
    return type.__new__(cls, name, bases, attrs)

class Class1(object):
    __metaclass__ = LoggedMeta
    def method1(self, *args):
        ...

However, I'm on Python 2.X, and the super() syntax doesn't work. At the time I call super, I don't have the __getattribute__'s class (but I do have its class name), so I can't use the old super syntax super(Class, Inst).

I tried earlier to use metaclasses, but override all the methods instead of __getattribute__, but I want to log static method calls also, and they gave me some trouble.

I searched for this type of question, but found no-one who tried changing a class this way.

Any ideas or help would be very appreciated.

EDIT: My solution was this (mostly taken from this thread):

import inspect, types

CLASS = 0
NORMAL = 1
STATIC = 2

class DecoratedMethod(object):

    def __init__(self, func, type_):
        self.func = func
        self.type = type_

    def __get__(self, obj, cls=None):
        def wrapper(*args, **kwargs):
            print "before"
            if self.type == CLASS:
                #classmethods (unlike normal methods) reach this stage as bound methods, but args still contains the class
                #as a first argument, so we omit it.
                ret = self.func(*(args[1:]), **kwargs)
            else:
                ret = self.func(*args, **kwargs)
            print "after"
            return ret
        for attr in "__module__", "__name__", "__doc__":
            setattr(wrapper, attr, getattr(self.func, attr))
        if self.type == CLASS:
            return types.MethodType(wrapper, cls, type)
        elif self.type == NORMAL:
            return types.MethodType(wrapper, obj, cls) 
        else:
            return wrapper

def decorate_class(cls):
    for name, meth in inspect.getmembers(cls):
        if inspect.ismethod(meth):
            if inspect.isclass(meth.im_self):
                # meth is a classmethod
                setattr(cls, name, DecoratedMethod(meth, CLASS))
            else:
                # meth is a regular method
                setattr(cls, name, DecoratedMethod(meth, NORMAL))
        elif inspect.isfunction(meth):
            # meth is a staticmethod
            setattr(cls, name, DecoratedMethod(meth, STATIC))
    return cls


@decorate_class
class MyClass(object):

    def __init__(self):
        self.a = 10
        print "__init__"

    def foo(self):
        print self.a

    @staticmethod
    def baz():
        print "baz"

    @classmethod
    def bar(cls):
        print "bar"

later I cleaned it up a bit, but that's the solution's essence. I need this difference between class, static and normal methods because I want to have

inst = MyClass()
assert type(inst.baz) == types.FunctionType
assert type(inst.foo) == types.MethodType
assert type(inst.bar) == types.MethodType
like image 838
WallE Avatar asked Jun 06 '13 12:06

WallE


2 Answers

Why don't you alter the class object?

You can go through the methods in a class with dir(MyClass) and replace them with a wrapped version... something like:

def logify(klass):
    for member in dir(klass):
        if not callable(getattr(klass, method))
            continue # skip attributes
        setattr(klass, method, log(method))

tinker around with something like this... should work...

like image 60
Daren Thomas Avatar answered Nov 11 '22 18:11

Daren Thomas


A class decorator can help here. Decorate the whole class and add you logging functionality to all callable attributes the class have.

like image 21
Mike Müller Avatar answered Nov 11 '22 16:11

Mike Müller