6

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).

4
  • Maybe self annotation? Define a protocol class AllTogether(QGraphicsItemProtocol, Protocol[VisibleDataType]): dataobject: VisibleDataType and then def update_visibility(self: AllTogether[VisibleDataType]) -> None:. That will add extra safety: if the subclass does not define/inherit setVisible, calls of update_visibility will be rejected. Commented Oct 25 at 19:53
  • mypy-play.net/…, should work the same way in pyright (but pyright online playground goes down/gets stuck too often, so just check that locally) Commented Oct 25 at 20:02
  • I really do not like Protocols, and I'd personally opt for if 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. Commented Oct 25 at 23:13
  • Also note that bindings often rely on delicate and low level implementations that may not always play fine with common/assumed syntax that code checkers may want. Simply put, not only you may need to find work-arounds specific to the binding (which may as well become broken at some point), but, also, it is possible that that same work-arounds would be ugly/unreliable. Not to mention that the same code checker may need further adjustments caused by the special needs that the binding not only wants, but requires to do. Besides, understanding interpreter consistency comes before type checking. Commented Oct 26 at 5:25

1 Answer 1

5

Qt’s C++ bindings and Python’s typing system just don’t like each other. You’ve basically got two metaclasses walking into a bar, and neither’s picking up the tab.

Long story short: you can’t safely mix Generic or Protocol with Qt classes. PySide6 has its own metaclass (ShibokenObjectType) that refuses to play nice with abc.ABCMeta or _ProtocolMeta. That’s where all the MRO drama comes from.

The usual hack is to lie a little during type checking, define a fake base class only when the type checker is actually running:

from typing import TYPE_CHECKING, Protocol, TypeVar, Generic

if TYPE_CHECKING:
    from PySide6.QtWidgets import QGraphicsItem as QGraphicsItemProtocol
else:
    QGraphicsItemProtocol = object

VisibleDataType = TypeVar("VisibleDataType", bound=VisibleData)

class HasDataobject(Generic[VisibleDataType], QGraphicsItemProtocol):
    dataobject: VisibleDataType
    def update_visibility(self):
        self.setVisible(self.dataobject.is_visible())

At runtime, QGraphicsItemProtocol is just object, so PySide doesn’t lose its mind. During static analysis, Pyright sees the proper Qt type and calms down.

If you want to be extra proper, define your own tiny protocol instead:

class AllTogether(Protocol, Generic[VisibleDataType]):
    dataobject: VisibleDataType
    def setVisible(self, visible: bool, /): ...
    def update_visibility(self) -> None: ...

Then just annotate your self:

def update_visibility(self: AllTogether[VisibleDataType]) -> None:

Bottom line: don’t make your mix-ins or protocols actually inherit from Qt classes, their metaclasses are allergic to cooperation. Fake it during TYPE_CHECKING, and both PySide and Pyright will finally stop screaming.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.