6

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

pppery
  • 3,731
  • 22
  • 33
  • 46
Janus Troelsen
  • 20,267
  • 14
  • 135
  • 196
  • 1
    I will try to dig deeper on that, but my first approach would be to create a custom type alias, along with the baseclass: https://mypy.readthedocs.io/en/latest/kinds_of_types.html#type-aliases – jsbueno Dec 12 '19 at 20:43
  • @JanusTroelsen I tried to create an MCVE with your "Union type on self" but I got `error: Item "QtPluginBase" of "Union[QtPluginBase, HW_PluginBase]" has no attribute "keystore_class"`. – aaron Dec 19 '19 at 15:22
  • @aaron Is that a bug from Mypy? I see `keystore_class` [defined in HW_PluginBase](https://github.com/spesmilo/electrum/blob/6b8c447eb90a0a37f7271681e98f319c8178592a/electrum/plugins/hw_wallet/plugin.py#L42), I don't know why Mypy doesn't recognize that? – Janus Troelsen Dec 19 '19 at 23:08
  • @JanusTroelsen It seems correct. `Union` means `or`. `QtPluginBase` doesn't have `keystore_class`. Why doesn't it subclass `HW_PluginBase` if its methods directly depend on it? – aaron Dec 20 '19 at 01:20

2 Answers2

5

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.

Janus Troelsen
  • 20,267
  • 14
  • 135
  • 196
  • 1
    I like this much better than my own answer! Hint: for python<3.8, there's a backport of protocols in `typing_extensions`, e.g. `if sys.version_info >= (3, 8): from typing import Protocol; else: from typing_extensions import Protocol`. – hoefling Feb 21 '20 at 12:54
1

Since mypy doesn't offer an Intersection type yet, you can't type the self arg correctly (and the Union is not a replacement for that!). What you can do is introducing base classes for mixins for type checking only. This is a trick I often use when working with mixins in Django projects. Example:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .plugin import HW_PluginBase
    _Base = HW_PluginBase
else:
    _Base = object


class QtPluginBase(_Base):
    def load_wallet(self, wallet: 'Abstract_Wallet', window: ElectrumWindow):
        ...

You can now drop the explicit typing of self since mypy can infer all necessary base classes itself.

hoefling
  • 59,418
  • 12
  • 147
  • 194