0

I was working on a decorator that decorates the class. It woks fine for instance methods but gives an TypeError for class method. The code is as below:

def deco_method(fn):
  def wrapper(*arg, **kwarg):
    """
    Function: Wrapper
    """
    print "Calling function {}".format(fn.__name__)
    print arg, kwarg
    ret_val = fn(*arg, **kwarg)
    print "Executed function {}".format(fn.__name__)
    return ret_val
  return wrapper

def clsdeco(cls):
  attributes = cls.__dict__.keys()

  for attribute in attributes:
    # Do not decorate private methods
    if '__' in attribute:
        continue 

    # Get the method
    value = getattr(cls, attribute)
    if not hasattr(value, '__call__'):
        continue

    # CHeck if method is a class method or normal method and decoate accordingly
    if value.im_self is None:# non class method
        setattr(cls, attribute, deco_method(value))
    elif value.im_self is cls: # CHeck if the method is class method 
        setattr(cls, attribute, classmethod(deco_method(value)))
    else: 
        assert False
  return cls # return decorated class



@clsdeco
class Person:
  message = "Hi Man"
  def __init__(self, first_name, last_name):
    self.fname = first_name
    self.lname = last_name
    self.age = None

  def get_name(self):
    print "Name is '{} {}'".format(self.fname, self.lname)

  @classmethod
  def greet_person(cls):
     print cls.message

p = Person('John', 'snow')
p.greet_person()

It gives an error:

TypeError: greet_person() takes exactly 1 argument (2 given)

If i remove @clsdeco, it works perfectly fine.

Any idea what i am missing here?

Luminos
  • 311
  • 1
  • 3
  • 12

1 Answers1

1

If you add the line shown it will work. This is because the @classmethod decorator applied in the class definition changes what getattr(cls, attribute) returns—it will be a descriptor for the named method which adds the cls argument and then calls the real method.

What you need to do is retrieve the "raw" value of the attribute which is just a regular function and then turn it back into a class method by explicitly calling classmethod. This needed "raw" value is stored in the class dictionary __dict__ associated with the same attribute name, hence the need for adding the value = cls.__dict__[attribute].__func__ line.

Something similar will also be required to handle static methods properly. How to do this for all the different types of methods is described in this answer to the question Decorating a method that's already a classmethod? Some of the other answers also describe what's going on in more detail than I have here.

def clsdeco(cls):
  attributes = cls.__dict__.keys()

  for attribute in attributes:
    # Do not decorate private methods
    if '__' in attribute:
        continue

    # Get the method
    value = getattr(cls, attribute)
    if not hasattr(value, '__call__'):
        continue

    # Check if method is a class method or normal method and decoate accordingly
    if value.im_self is None:# non class method
        setattr(cls, attribute, deco_method(value))
    elif value.im_self is cls: # Check if the method is class method
        value = cls.__dict__[attribute].__func__                        # ADDED
        setattr(cls, attribute, classmethod(deco_method(value)))
    else:
        assert False

  return cls # return decorated class
Community
  • 1
  • 1
martineau
  • 119,623
  • 25
  • 170
  • 301
  • Thanks! I did not notice that the getattr returns the descriptors. "value = cls.__dict__[attribute].__func__" works. Alternatively we can also do "value = value.im_func" [for python 2.7] and "value = value.__func__" [python 3] – Luminos May 17 '17 at 06:05
  • @Luminos: In Python 2.6 they made `__func__` an alais for `im_func`, so using that would be more portable—but I believe using `cls.__dict__[attribute].__func__` already will work in both Python 2 and 3. – martineau May 18 '17 at 16:48