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 @abstractmethod
s 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()
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
....