Search code examples
pythonpython-3.xmultiple-inheritancegeneric-programmingmypy

How to get Mypy working with multiple mixins relying on each other?


Currently in Electrum we use the Union type on self to be able to access methods from multiple mixed-in parent classes. For example, QtPluginBase relies on being mixed into a subclass of HW_PluginBase to work. For example, a valid use is class TrezorPlugin(QtPluginBase, HW_PluginBase).

There is the Qt gui, the Kivy gui, and there is also CLI. Although hardware wallets are not implemented for Kivy, they could be in the future. You can already use them on the CLI.

However there are also multiple hardware wallet manufacturers, all with their own plugins.

Consider Trezor + Qt:

For Qt, we have this class hierarchy:

  • electrum.plugins.hw_wallet.qt.QtPluginBase used by
  • electrum.plugins.trezor.qt.QtPlugin(QtPluginBase)

For Trezor, we have:

  • electrum.plugin.BasePlugin used by
  • electrum.plugins.hw_wallet.plugin.HW_PluginBase(BasePlugin) used by
  • electrum.plugins.trezor.trezor.TrezorPlugin(HW_PluginBase)

And to create the actual Qt Trezor plugin:

  • electrum.plugins.trezor.qt.Plugin(TrezorPlugin, QtPlugin)

The point is that the base gui-neutral plugin will first gain manufacturer-specific methods; then it will gain gui-specific methods.

Aaron (in the comments) suggests that QtPluginBase could subclass HW_PluginBase, but that would mean that the manufacturer-specific stuff would come after, which means the resulting classes cannot be used by the CLI or Kivy.

Note that both

electrum.plugins.trezor.trezor.TrezorPlugin(HW_PluginBase)

and

electrum.plugins.hw_wallet.qt.QtPluginBase

rely on HW_PluginBase. They can't both subclass it.

So if we avoid mix-ins, then the only alternative would be to either have QtPluginBase subclass TrezorPlugin (but there are many manufacturers), or TrezorPlugin could subclass QtPluginBase but then, again, the resulting classes cannot be used by the CLI or Kivy.

I realize that Union is an "or", so the hint is indeed not making sense. But there is no Intersection type. With Union, most of the PyCharm functionality works.

One thing that would be nice is if QtPluginBase could have a type-hint that it subclasses HW_PluginBase, but without actually subclassing at runtime.

How could this be typed with Mypy without having to use this hacky Union type hint on every method (since every method has self)?


Solution

  • With the Protocols added in PEP-544 (Python 3.8+), you can define the intersection interface yourself! This also lets you hide implementation details in ClassA that you don't want ClassB to use.

    from typing import Protocol
    
    class InterfaceAB(Protocol):
        def method_a(self) -> None: ...
        def method_b(self) -> None: ...
    
    class ClassA:
        def method_a(self) -> None:
            print("a")
    
    class ClassB:
        def method_b(self: InterfaceAB) -> None:
            print("b")
            self.method_a()
    
    # if I remove ClassA here, I get a type checking error!
    class AB(ClassA, ClassB): pass
    
    ab = AB()
    ab.method_b()
    
    # % mypy --version
    # mypy 0.761
    # % mypy mypy-protocol-demo.py
    # Success: no issues found in 1 source file
    

    Credits to SomberNight/ghost43 for the initial version of this file.