pythonabstract-classmultiple-inheritancemetaclasspyside6

Creating a metaclass that inherits from ABCMeta and QObject


I am building an app in PySide6 that will involve dynamic loading of plugins. To facilitate this, I am using ABCMeta to define a custom metaclass for the plugin interface, and I would like this custom metaclass to inherit from ABC and from QObject so that I can abstract as much of the behavior as possible, including things like standard signals and slots that will be common to all subclasses.

I have set up a MWE that shows the chain of logic that enabled me to get this setup working, but the chain of inheritance goes deeper than I thought it would, and the @abstractmethod enforcement of ABC does not seem to carry through (in the sense that not overriding print_value() does not cause an error). Is it possible to shorten this while still having the desired inheritance? In the end, my goal is to have the MetaTab class as an abstract base class that inherits from ABC so that I can define @abstractmethods inside it, and then subclass that for individual plugins. Do I really need both QABCMeta and QObjectMeta to make this work, or is there a way to clean it up that eliminates one of the links in this inheritance chain?

from abc import ABC, ABCMeta, abstractmethod
from PySide6.QtCore import QObject

class QABCMeta(ABCMeta, type(QObject)):
    pass

class QObjectMeta(ABC, metaclass=QABCMeta):
    pass

class MetaTab(QObject, QObjectMeta):
    def __init__(self, x):
        print('initialized')
        self.x = x

    @abstractmethod
    def print_value(self):
        pass

class Tab(MetaTab):
    def __init__(self, x):
        super().__init__(x)

    def print_value(self):
        print(self.x)
        
def main():
    obj = Tab(5)
    for b in obj.__class__.__bases__:
        print("Base class name:", b.__name__)
    print("Class name:", obj.__class__.__name__)
    obj.print_value()


if __name__=='__main__':
    main()

Solution

  • Doing some tests here: PySide6 Qobjetc inheritance modifies the usual Python behavior for classes, including attribute lookup - and this renders the mechanisms for abstractmethods in Python's ABC inoperative. Metclasses are a complicated subject, and cooperative metaclasses between different projects are even more complicated.

    The workaround I found is to re-implement some of the behavior of ABCMeta in the metaclass that combines the metaclass for QOBjects and ABCMeta itself - with the changes bellow, the @abstractmethod behavior is restored:

    import abc
    from PySide6.QtCore import QObject
    
    class QABCMeta(abc.ABCMeta, type(QObject)):
        def __new__(mcls, name, bases, ns, **kw):
            cls = super().__new__(mcls, name, bases, ns, **kw)
            abc._abc_init(cls)
            return cls
        def __call__(cls, *args, **kw):
            if cls.__abstractmethods__:
                raise TypeError(f"Can't instantiate abstract class {cls.__name__} without an implementation for abstract methods {set(cls.__abstractmethods__)}")
            return super().__call__(*args, **kw)
    
    

    Testing on the REPL with a class derived from "MetaTab" which doesn't implement print_value will raise as expected:

    In [112]: class T2(MetaTab):
         ...:     pass
         ...:
    
    In [113]: T2(1)
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    Cell In[113], line 1
    ----> 1 T2(1)
    
    Cell In[105], line 11, in QABCMeta.__call__(cls, *args, **kw)
    (...)
    
    Can't instantiate abstract class T2 without an implementation for abstract methods : {'print_value'})
    
    

    As for the hierarchy you build: you probably could skip some of the intermediate classes, but what I´d change there is to avoid using the name "Meta" for classes that are not metaclasses (i.e. classes used to build the classes themselves, and that are passed to the metaclass= named parameter) - both QObjectMeta and MetaTab are confusing names due to that - you could use an infix like "Base" or "Mixin" instead.

    That said, the class you call QobjectMeta could be just:

    class AQObjectBase(QObject, metaclass=QABCMeta): 
        pass
    
    

    And then your "MetaTab" would not need to inherit explicitly from QObject. (explicit inheritance from ABC with the metaclass behavior restores isn´t needed):

    class TabBase(AQObjectBase):
         def __init__(...):
             ...
         @abstractmethod
         ....