Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 495
user7624126 Avatar asked Feb 11 '19 16:02

user7624126


1 Answers

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!

like image 78
user7624126 Avatar answered Nov 07 '22 10:11

user7624126