Получить mypy для принятия подтипа родового типа в качестве аргумента метода

Я пытаюсь извлечь шаблон, который мы используем в нашей базе кода, в более общую конструкцию многократного использования. Однако я не могу заставить аннотации общего типа работать с mypy.

Вот что я получил:

from abc import (
    ABC,
    abstractmethod
)
import asyncio
import contextlib
from typing import (
    Any,
    Iterator,
    Generic,
    TypeVar
)

_TMsg = TypeVar('_TMsg')

class MsgQueueExposer(ABC, Generic[_TMsg]):

    @abstractmethod
    def subscribe(self, subscriber: 'MsgQueueSubscriber[_TMsg]') -> None:
        raise NotImplementedError("Must be implemented by subclasses")

    @abstractmethod
    def unsubscribe(self, subscriber: 'MsgQueueSubscriber[_TMsg]') -> None:
        raise NotImplementedError("Must be implemented by subclasses")


class MsgQueueSubscriber(Generic[_TMsg]):

    @contextlib.contextmanager
    def subscribe(
            self,
            msg_queue_exposer: MsgQueueExposer[_TMsg]) -> Iterator[None]:
        msg_queue_exposer.subscribe(self)
        try:
            yield
        finally:
            msg_queue_exposer.unsubscribe(self)


class DemoMsgQueSubscriber(MsgQueueSubscriber[int]):
    pass

class DemoMsgQueueExposer(MsgQueueExposer[int]):

    # The following works for mypy:

    # def subscribe(self, subscriber: MsgQueueSubscriber[int]) -> None:
    #     pass

    # def unsubscribe(self, subscriber: MsgQueueSubscriber[int]) -> None:
    #     pass

    # This doesn't work but I want it to work :)

    def subscribe(self, subscriber: DemoMsgQueSubscriber) -> None:
        pass

    def unsubscribe(self, subscriber: DemoMsgQueSubscriber) -> None:
        pass

Я прокомментировал какой-то код, который работает, но не полностью удовлетворяет мои потребности. В основном я хочу, чтобы DemoMsgQueueExposer DemoMsgQueSubscriber в своих методах subscribe и DemoMsgQueSubscriber unsubscribe. Тип кода отлично проверяется, если я использую MsgQueueSubscriber[int] как тип, но я хочу, чтобы он принимал подтипы этого.

Я продолжаю сталкиваться со следующей ошибкой.

generic_msg_queue.py:55: error: Argument 1 of "subscribe" incompatible with supertype "MsgQueueExposer"

Я чувствую, что это имеет какое-то отношение к co-/контравариантам, но я попробовал несколько вещей, прежде чем я сдался и пришел сюда.

Ответ 1

Ваши лучшие ставки заключаются в том, чтобы: 1) просто удалить subscribe и unsubscribe от unsubscribe от MsgQueueExposer вообще или 2) сделать MsgQueueExposer общим для абонента, либо в дополнение, либо вместо msg.

Ниже приведен пример того, как может выглядеть подход 2, предполагая, что мы хотим сохранить параметр типа _TMsg. Обратите внимание, что я добавил метод messages() для демонстрационных целей:

from abc import ABC, abstractmethod
import asyncio
import contextlib
from typing import Any, Iterator, Generic, TypeVar, List

_TMsg = TypeVar('_TMsg')
_TSubscriber = TypeVar('_TSubscriber', bound='MsgQueueSubscriber')

class MsgQueueExposer(ABC, Generic[_TSubscriber, _TMsg]):

    @abstractmethod
    def subscribe(self, subscriber: _TSubscriber) -> None:
        raise NotImplementedError("Must be implemented by subclasses")

    @abstractmethod
    def unsubscribe(self, subscriber: _TSubscriber) -> None:
        raise NotImplementedError("Must be implemented by subclasses")

    @abstractmethod
    def messages(self) -> List[_TMsg]:
        raise NotImplementedError("Must be implemented by subclasses")


class MsgQueueSubscriber(Generic[_TMsg]):
    # Note that we are annotating the 'self' parameter here, so we can
    # capture the subclass exact type.

    @contextlib.contextmanager
    def subscribe(
            self: _TSubscriber,
            msg_queue_exposer: MsgQueueExposer[_TSubscriber, _TMsg]) -> Iterator[None]:
        msg_queue_exposer.subscribe(self)
        try:
            yield
        finally:
            msg_queue_exposer.unsubscribe(self)


class DemoMsgQueSubscriber(MsgQueueSubscriber[int]):
    pass

class DemoMsgQueueExposer(MsgQueueExposer[DemoMsgQueSubscriber, int]):
    def subscribe(self, subscriber: DemoMsgQueSubscriber) -> None:
        pass

    def unsubscribe(self, subscriber: DemoMsgQueSubscriber) -> None:
        pass

    def messages(self) -> List[int]:
        pass

В более широком смысле мы хотели выразить мысль о том, что каждый MsgQueueExposer работает только для определенного типа подписчика, поэтому нам нужно было где-то закодировать эту информацию.

Одно отверстие в этом заключается в том, что mypy не сможет убедиться, что когда вы используете MsgQueueExposer что независимо от того, какой тип получает абонент, и что независимо от того, какой тип ожидающий будет ожидать, согласятся. Итак, если мы определили демо-подписчика как class DemoMsgQueSubscriber(MsgQueueSubscriber[str]) но сохранили DemoMsgQueueExposer же, mypy не смог бы обнаружить эту ошибку.

Но я предполагаю, что вы всегда будете создавать новый подписчик и новый экспонент в парах и это то, что вы можете тщательно проверить, так что эта ошибка, вероятно, вряд ли произойдет на практике.