I’d like to create a Python class decorator (*) that would be able to seamlessly wrap all method types the class might have: instance, class and static.
This is the code I have for now, with the parts that break it commented:
def wrapItUp(method):
def wrapped(*args, **kwargs):
print "This method call was wrapped!"
return method(*args, **kwargs)
return wrapped
dundersICareAbout = ["__init__", "__str__", "__repr__"]#, "__new__"]
def doICareAboutThisOne(cls, methodName):
return (callable(getattr(cls, methodName))
and (not (methodName.startswith("__") and methodName.endswith("__"))
or methodName in dundersICareAbout))
def classDeco(cls):
myCallables = ((aname, getattr(cls, aname)) for aname in dir(cls) if doICareAboutThisOne(cls, aname))
for name, call in myCallables:
print "*** Decorating: %s.%s(...)" % (cls.__name__, name)
setattr(cls, name, wrapItUp(call))
return cls
@classDeco
class SomeClass(object):
def instanceMethod(self, p):
print "instanceMethod: p =", p
@classmethod
def classMethod(cls, p):
print "classMethod: p =", p
@staticmethod
def staticMethod(p):
print "staticMethod: p =", p
instance = SomeClass()
instance.instanceMethod(1)
#SomeClass.classMethod(2)
#instance.classMethod(2)
#SomeClass.staticMethod(3)
#instance.staticMethod(3)
I’m having two issues trying to make this work:
- When iterating over all callables, how do I find out if it is of an instance, class or static type?
- How to I overwrite the method with a proper wrapped version of it that is invoked correctly for each of those cases?
Currently, this code generates different TypeErrors depending on what commented snippet is uncommented, like:
TypeError: unbound method wrapped() must be called with SomeClass instance as first argument (got int instance instead)TypeError: classMethod() takes exactly 2 arguments (3 given)
(*): The same problem is much simpler if you’re decorating the methods directly.
Because methods are wrappers for functions, to apply a decorator to a method on a class after the class has been constructed, you have to:
im_funcattribute.It is difficult to distinguish a
classmethodfrom a regular method once the@classmethoddecorator has been applied; both kinds of methods are of typeinstancemethod. However, you can check theim_selfattribute and see whether it isNone. If so, it’s a regular instance method; otherwise it’s aclassmethod.Static methods are simple functions (the
@staticmethoddecorator merely prevents the usual method wrapper from being applied). So you don’t have to do anything special for these, it looks like.So basically your algorithm looks like this:
types.MethodType? If so, it is either a class method or an instance method.im_selfisNone, it is an instance method. Extract the underlying function via theim_funcattribute, decorate that, and re-apply the instance method:meth = types.MethodType(func, None, cls)im_selfis notNone, it is a class method. Exctract the underlying function viaim_funcand decorate that. Now you have to reapply theclassmethoddecorator but you can’t becauseclassmethod()doesn’t take a class, so there’s no way to specify what class it will be attached to. Instead you have to use the instance method decorator:meth = types.MethodType(func, cls, type). Note that thetypehere is the actual built-in,type.types.MethodTypethen it is a static method or other non-bound callable, so just decorate it.These change somewhat in Python 3 — unbound methods are functions there, IIRC. In any case this will probably need to be completely rethought there.