Just when I though I understood metaclasses...
Disclaimer: I have looked around for an answer before posting, but most of the answers I have found are about calling super() to get at another @classmethod in the MRO (no metaclass involved) or, surprisingly, a lot of them were about trying to do something in metaclass.__new__ or metaclass.__call__ which meant the class wasn't fully created yet. I'm pretty sure (let's say 97%) that this is not one of those problems.
Environment: Python 3.7.2
The problem:
I have a metaclass FooMeta that defines a method get_foo(cls), a class Foo that is built from that metaclass (so an instance of FooMeta) and has a @classmethod get_bar(cls). Then another class Foo2 that inherits from Foo. In Foo2, I subclass get_foo by declaring it a @classmethod and calling super(). This fails miserably...
i.e. with this code
class FooMeta(type):
    def get_foo(cls):
        return 5
class Foo(metaclass=FooMeta):
    @classmethod
    def get_bar(cls):
        return 3
print(Foo.get_foo)
# >>> <bound method FooMeta.get_foo of <class '__main__.Foo'>>
print(Foo.get_bar)
# >>> <bound method Foo.get_bar of <class '__main__.Foo'>>
class Foo2(Foo):
    @classmethod
    def get_foo(cls):
        print(cls.__mro__)
        # >>> (<class '__main__.Foo2'>, <class '__main__.Foo'>, <class 'object'>)
        return super().get_foo()
    @classmethod
    def get_bar(cls):
        return super().get_bar()
print(Foo2().get_bar())
# >>> 3
print(Foo2().get_foo())
# >>> AttributeError: 'super' object has no attribute 'get_foo'
The question:
So with my class being an instance of the metaclass, and having verified that both class methods exist on the class Foo, why aren't both calls to the super().get_***() working inside Foo2? What am I not understanding about metaclasses or super() that's preventing me from finding these results logical?
EDIT: Further testing shows that the methods on Foo2 being class methods or instance methods doesn't change the result.
EDIT 2: Thanks to @chepner's answer, I think the problem was that super() was returning a super object representing Foo (this is verified with super().__thisclass__) and I was expecting super().get_foo() to behave (maybe even to call) get_attr(Foo, 'get_foo') behind the scene. It seems that it isn't... I'm still wondering why, but it is getting clearer :)
Foo may have a get_foo method, but super isn't designed to check what attributes a superclass has. super cares about what attributes originate in a superclass.
To understand super's design, consider the following multiple inheritance hierarchy:
class A:
    @classmethod
    def f(cls):
        return 1
class B(A):
    pass
class C(A):
    @classmethod
    def f(cls):
        return 2
class D(B, C):
    @classmethod
    def f(cls):
        return super().f() + 1
A, B, C, and D all have an f classmethod, but B's f is inherited from A. D's method resolution order, the sequence of classes checked for attribute lookup, goes (D, B, C, A, object).
super().f() + 1 searches the MRO of cls for an f implementation. The one it should find is C.f, but B has an inherited f implementation, and B is before C in the MRO. If super were to pick up B.f, this would break C's attempt to override f, in a situation commonly referred to as the "diamond problem".
Instead of looking at what attributes B has, super looks directly in B's __dict__, so it only considers attributes actually provided by B instead of by B's superclasses or metaclasses.
Now, back to your get_foo/get_bar situation. get_bar comes from Foo itself, so super().get_bar() finds Foo.get_bar. However, get_foo is provided not by Foo, but by the FooMeta metaclass, and there is no entry for get_foo in Foo.__dict__. Thus, super().get_foo() finds nothing.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With