The following code-snippet bridges some dataclasses and GUI-classes using PySide6 (the Qt library).
The HasDataobject class is key here. It defines a mix-in for subclasses of QGraphicsItem. It adds an attribute (dataobject) and method (update_visibility()) to those classes.
from typing import TypeVar, Generic, Protocol
from PySide6.QtWidgets import QGraphicsItem, QGraphicsEllipseItem
# QGraphicsEllipseItem is a subclass of QGraphicsItem
### Data objects ###
class VisibleData:
def is_visible(self) -> bool:
return True
class MyNode(VisibleData):
pass
VisibleDataType = TypeVar('VisibleDataType', bound=VisibleData)
### Visual objects (using PySide6) ###
class QGraphicsItemProtocol(Protocol):
"""Define the methods of QGraphicsItem that HasDataobject uses."""
def setVisible(self, visible: bool, /):
...
class HasDataobject(Generic[VisibleDataType]):
"""Mix-in class. Adds an update_visibility() method, and
a dataobject attribute. The dataobject must have a
is_visible() method, as defined in VisibleData.
Any subclass of HasDataobject must also be a subclass of
QGraphicsItem, which defines setVisible()."""
dataobject: VisibleDataType
def update_visibility(self):
self.setVisible(self.dataobject.is_visible())
class Circle(QGraphicsEllipseItem, HasDataobject[MyNode]):
def __init__(self):
super().__init__()
pass
The above code works without issues. However, Pyright or other code validators will complain that setVisible is not a known method (reportAttributeAccessIssue):
self.setVisible(self.dataobject.is_visible())
The easiest way to suppress this is to add # type: ignore, but I prefer to make it explicit what is described in the docstring: Any subclass of HasDataobject must also be a subclass of QGraphicsItem.
My initial thought was to make HasDataobject a subclass of QGraphicsItem:
class HasDataobject(QGraphicsItem, Generic[VisibleDataType])
However, that lead to the following RuntimeError in Circle.__init__ method:
RuntimeError: You can't initialize an PySide6.QtWidgets.QGraphicsEllipseItem object in class Circle twice!
So my second attempt is to use the QGraphicsItemProtocol define above:
class HasDataobject(Generic[VisibleDataType], QGraphicsItemProtocol)
However, that gives a
TypeError: Cannot create a consistent method resolution order (MRO) for bases Generic, QGraphicsItemProtocol
So next I tried to reverse the two base classes:
class HasDataobject(QGraphicsItemProtocol, Generic[VisibleDataType])
However, that again leads to a problem when defining the Circle class:
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
I'm a bit stuck now. How can I use type hints in this code and make Pyright (and myself) happy?
PS: Any suggestions and best practices are appreciated. I even toyed with the idea to make HasDataobject a genuine class (has-a QGraphicsItem instead of is-a QGraphicsItem) instead of a mix-in, but subclassing is really beneficial since that enables the power of Qt with things like scene.addItem(a_circle).
selfannotation? Define a protocolclass AllTogether(QGraphicsItemProtocol, Protocol[VisibleDataType]): dataobject: VisibleDataTypeand thendef update_visibility(self: AllTogether[VisibleDataType]) -> None:. That will add extra safety: if the subclass does not define/inheritsetVisible, calls ofupdate_visibilitywill be rejected.Protocols, and I'd personally opt forif TYPE_CHECKING: from PySide6.QtWidgets import QGraphicsItem as QGraphicsItemProtocol\nelse: QGraphicsItemProtocol = object. I'm speaking from the point of view of internal (not library) code though.