Search code examples
pythondecoratorchaining

Decorating a method that's already a classmethod?


I had an interesting problem this morning. I had a base class that looked like this:

# base.py
class Base(object):

    @classmethod
    def exists(cls, **kwargs):
        # do some work
        pass

And a decorator module that looked like this:

# caching.py

# actual caching decorator
def cached(ttl):
    # complicated

def cached_model(ttl=300):
    def closure(model_class):
        # ...
        # eventually:
        exists_decorator = cached(ttl=ttl)
        model_class.exists = exists_decorator(model_class.exists))

        return model_class
    return closure

Here's my subclass model:

@cached_model(ttl=300)
class Model(Base):
    pass

Thing is, when I actually call Model.exists, I get complaints about the wrong number of arguments! Inspecting the arguments in the decorator shows nothing weird going on - the arguments are exactly what I expect, and they match up with the method signature. How can I add further decorators to a method that's already decorated with classmethod?

Not all models are cached, but the exists() method is present on every model as a classmethod, so re-ordering the decorators isn't an option: cached_model can add classmethod to exists(), but then what makes exists() a classmethod on uncached models?


Solution

  • The classmethod decorator actually prepends a class argument to calls to the method, in certain circumstances, as far as I can tell, in addition to binding the method to the class. The solution was editing my class decoration closure:

    def cached_model(ttl=300):
        def closure(model_class):
            # ...
            # eventually:
            exists_decorator = cached(ttl=ttl, cache_key=exists_cache_key)
            model_class.exists = classmethod(exists_decorator(model_class.exists.im_func))
    
            return model_class
        return closure
    

    The im_func property appears to get a reference to the original function, which allows me to reach in and decorate the original function with my caching decorator, and then wrap that whole mess in a classmethod call. Summary, classmethod decorations are not stackable, because arguments seem to be injected.