Search code examples
pythonstatic-methodsmetaclassclass-method

How can I check whether a method is a class method or a static method in a metaclass?


Here is a very simple Base class, which contains a static method and a class method:

class Base():

    @staticmethod
    def f():
        print("Base.f")

    @classmethod
    def g(cls):
        print("Base.g")

    def h(self):
        print("Base.h")

If a class is to be derived from Base and override either f or g then, the staticmethod and classmethod decorators need to be used again on the overriding methods.

class A(Base):

    @staticmethod
    def f():
        print("A.f")

class B(Base):

    @classmethod
    def g(cls):
        print("B.g")

So, at first I thought I would create a metaclass that automatically makes f a staticmethod and g a staticmethod.

class BaseMeta(type):

    def __init__(cls, name, bases, namespace):
        super().__init__(name, bases, namespace)
        if 'f' in namespace: cls.f = staticmethod(cls.f)
        if 'g' in namespace: cls.g = classmethod(cls.g)

Now the rest of the classes don't need to use staticmethod and classmethod explicitly.

class Base(metaclass=BaseMeta):

    def f():
        print("Base.f")

    def g(cls):
        print("Base.g")

    def h(self):
        print("Base.h")

class A(Base):

    def f():
        print("A.f")

class B(Base):

    def g(cls):
        print("B.g")

This works, but I don't like how it looks. Now, I realize that the staticmethod and classmethod decorators should be explicitly used (after all, explicit is better than implicit, isn't it?)

So I thought I could keep the metaclass, but this time instead of enforcing the decorators, I should just check whether or not they have been used and throw an exception if they haven't.

class BaseMeta(type):

    def __init__(cls, name, bases, namespace):
        super().__init__(name, bases, namespace)
        # check if cls.f is a static method
        if not inspect.isfunction(cls.f):
            raise Exception("f should be a static method")
        # check if cls.g is a static method
        if not (inspect.ismethod(cls.g) and cls.g.__self__ == cls):
            raise Exception("g should be a class method")

Unfortunately, this doesn't work. It seems that in the metaclasse's __init__, everything is considered to be just a function (just printing cls.f and cls.g makes this apparent).

Is there something I'm missing here?


Solution

  • This:

    if not (inspect.ismethod(cls.g) and cls.g.__self__ == cls):
        raise Exception("g should be a class method")
    

    works fine, but this:

    if not inspect.isfunction(cls.f):
        raise Exception("f should be a static method")
    

    doesn't, because on Python 3, cls.f will be the f function whether or not the staticmethod decorator was applied. (On Python 2, it would have been an unbound method object without the decorator.)


    Instead of accessing cls.f or cls.g and trying to work out what kind of descriptor you went through based on the results of the descriptor protocol, bypass the descriptor protocol and access the raw contents of the class definition's namespace:

    if 'f' in namespace and not isinstance(namespace['f'], staticmethod):
        whatever()
    if 'g' in namespace and not isinstance(namespace['g'], classmethod):
        whatever()