14

So my problem is That when I have a class of type A that does things and I use those functions as a subclass(B) they are still typed for class A and do not accept my class B object as arguments or as function signature.

My problem simplified:

from typing import TypeVar, Generic, Callable

T = TypeVar('T')


class Signal(Generic[T]):
    def connect(self, connector: Callable[[T], None]) -> None:
        pass

    def emit(self, payload: T):
        pass


class A:
    def __init__(self) -> None:
        self.signal = Signal[A]()

    def do(self) -> None:
        self.signal.emit(self)

def handle_b(b: "B") -> None:
    print(b.something)

class B(A):
    def __init__(self) -> None:
        super().__init__()
        self.signal.connect(handle_b)

    @property
    def something(self) -> int:
        return 42

I can provide the complete signal class as well but that just distracts from the problem. This leaves me with one error in mypy:

error: Argument 1 to "connect" of "Signal" has incompatible type Callable[[B], None]; expected Callable[[A], None]

Since the signal handling is implemented in A the subclass B can't expect B type objects to be returned even though it clearly should be fine...

Neuron
  • 5,141
  • 5
  • 38
  • 59
user2799096
  • 151
  • 1
  • 1
  • 7
  • Well, the error is correct, you did constrain `self.signal` to `A` only: `self.signal = Signal[A]()`. – Martijn Pieters Dec 19 '17 at 22:55
  • You basically set `T = A` there. `B` may be a subclass, but because `A.something` doesn't exist, you can't use that attribute. You tied everything down to `A` now. A method that accepts `B` is obviously allowed to use `B.something` so the base class `A` can't ever satisfy that requirement. – Martijn Pieters Dec 19 '17 at 22:57

3 Answers3

1

The connector passed to Signal[A] is of type Callable[[A], None], which means it has to promise to be able to handle any instance of A (or any of it's sub-classes). handle_b cannot fulfill this promise, since it only works for instances of B, it therefore cannot be used as a connector for a signal of type Signal[A].

Presumably, the connector of the signal of any instance of B will only ever be asked to handle an instance of B, it therefore doesn't need to be of type Signal[A], but Signal[B] would be sufficient. This means the type of signal is not fixed, but varies for different sub-classes of A, this means A needs to be generic.

The answer by ogurets correctly makes A generic, however there is no a problem with do, since it's unclear whether self is of type expected by self.signal.emit. We can promise that these types will always match by annotating self with the same type variable used for Signal. By using a new type variable _A which is bound by A, we tell mypy that self will always be a subtype of A and therefore has a property signal.

from __future__ import annotations

from collections.abc import Callable
from typing import TypeVar, Generic

T = TypeVar('T')


class Signal(Generic[T]):
    def connect(self, connector: Callable[[T], None]) -> None:
        pass

    def emit(self, payload: T):
        print(payload)

_A = TypeVar('_A', bound='A')

class A(Generic[_A]):
    signal: Signal[_A]

    def __init__(self) -> None:
        self.signal = Signal[_A]()

    def do(self) -> None:
        self.signal.emit(self)

def handle_b(b: "B") -> None:
    print(b.something)

class B(A['B']):
    def __init__(self) -> None:
        super().__init__()
        self.signal.connect(handle_b)

    @property
    def something(self) -> int:
        return 42

b = B()
reveal_type(b.signal) # Revealed type is '...Signal[...B*]'
unique2
  • 2,162
  • 2
  • 18
  • 23
0

The type hint error is entirely correct. You created a Signal instance with A as the type, in the __init__ method of A:

self.signal = Signal[A]()

Passing in a subclass is fine, but all code interacting with that Signal instance now has to work for A instances only. handle_b() on the other handrequires an instance of B, and can't lower the requirement to A instead.

Drop the constraint:

self.signal = Signal()

or create an instance in each subclass with the correct type.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • shouldn't there be a way to tell the type Hinting to accept subtypes of A? I mean disabling the typing works, but then why have type hinting at all... – user2799096 Dec 19 '17 at 23:08
  • Sorry, had to step away from the keyboard for a while; it probably is possible to use a `TypeVar()` marked as covariant instead of `A` but won’t be able to test until tomorrow. – Martijn Pieters Dec 20 '17 at 00:02
  • I did already try that, I get a problem.py:10: error: Cannot use a covariant type variable as a parameter on the line: def emit(self, payload: T): And still have the old error :D maybe upgrade my mypy version... – user2799096 Dec 20 '17 at 10:18
0
from __future__ import annotations
from typing import TypeVar, Generic, Callable

T = TypeVar('T')


class Signal(Generic[T]):
    def connect(self, connector: Callable[[T], None]) -> None:
        pass

    def emit(self, payload: T):
        pass


class A(Generic[T]):
    def __init__(self) -> None:
        self.signal = Signal[T]()

    def do(self: A) -> None:
        self.signal.emit(self)


def handle_b(b: B) -> None:
    print(b.something)


class C:
    pass


def handle_c(c: C) -> None:
    print(c)


class B(A[B]):
    def __init__(self) -> None:
        super().__init__()
        self.signal.connect(handle_b)  # OK
        self.signal.connect(handle_c)  # incompatible type

    @property
    def something(self) -> int:
        return 42
ogurets
  • 618
  • 1
  • 12
  • 20
  • nice idea, but I still have errors on the emit of class A and the emit function definition itself, with mypy 0.600 – user2799096 May 25 '19 at 09:58
  • Hmm.. Last version of pip's mypy (0.5.0) looks fine. Sadly, too busy to dive deeper into this for now. – ogurets May 26 '19 at 18:03