pythonpyqt5python-typingpyside2metaclass

How do I use generic typing with PyQt subclass without metaclass conflicts?


I had tried the abc.ABCMeta with sip wrapper type, and it works well when subclass with abc.ABC.

class QABCMeta(wrappertype, ABCMeta):
    pass

class WidgetBase(QWidget, metaclass=QABCMeta):
    ...

class InterfaceWidget(WidgetBase, ABC):
    ...

class MainWidget(InterfaceWidget):
    ...

But it is not works on typing.Generic.

class QGenericMeta(wrappertype, GenericMeta):
    pass

class WidgetBase(QWidget, Generic[T], metaclass=QGenericMeta):
    ...

class GenericWidget(WidgetBase[float]):
    ...

It raised:

line 980, in __new__
    self if not origin else origin._gorg)
TypeError: can't apply this __setattr__ to sip.wrappertype object

I expected it to use generic subclass as usual:

class TableBase(QTableWidget, Generic[T]):
    @abstractmethod
    def raw_item(self, row: int) -> T:
        ...
    def data(self) -> Iterator[T]:
        yield from (self.raw_item(row) for row in range(self.rowCount()))

class MainTable(TableBase[float]):
    def raw_item(self, row: int) -> float:
        return float(self.item(row, 1).text())  # implementation

table = MainTable()
for data in table.data():
    data: float

But the data is still Any when without inherit Generic[T].

Can it solved with PEP 560 to do type checking?


Solution

  • Well, I found the answer.

    Since the metaclass of typing.Generic is abc.ABC, it should based on abc.ABCMeta too. But this is only works with Python 3.7 or above.

    And then, just use type(QObject) instead of sip.wrappertype:

    # -*- coding: utf-8 -*-
    
    from abc import abstractmethod, ABC, ABCMeta
    from typing import TypeVar, Generic, Iterator
    from PyQt5.QtCore import QObject
    from PyQt5.QtWidgets import QTableWidget
    
    QObjectType = type(QObject)
    T = TypeVar('T')
    
    
    class QABCMeta(QObjectType, ABCMeta):
        pass
    
    
    class BaseWidget(QTableWidget, Generic[T], metaclass=QABCMeta):
    
        @abstractmethod
        def raw_item(self, row: int) -> T:
            ...
    
        def data(self) -> Iterator[T]:
            yield from (self.raw_item(row) for row in range(self.rowCount()))
    
    
    class TestWidget(BaseWidget[float], ABC):  # optional inherit ABC.
    
        def raw_item(self, row: int) -> float:
            return float(self.item(row, 1).text())
    
    
    if __name__ == '__main__':
        w = TestWidget()
        for f in w.data():
            pass
    

    This code is works for PyCharm IDE, the annotation of variable f is float.

    When change PyQt5 to PySide2, it also works!