Выполнять универсальные функции на определенной типизированной конечной точке REST, сохраняя типы - PullRequest
0 голосов
/ 01 ноября 2018

Я делаю типизированную библиотеку REST, в которой все конечные точки имеют определенные классы и их методы установлены на объекте. Скажем, у нас есть список строк, возвращаемых конечной точкой A, он будет иметь класс MVCE A ниже. Я добавляю методы, чтобы все конечные точки работали в классе Base, чтобы конечные точки включали в себя как можно меньше шаблонного шаблона.

Однако есть некоторые функции, которые мне нужно выполнять на всех конечных точках списка, например A и B ниже, но не C. Эта общая функция get_all, поэтому мы получаем все объекты из списка.

Проблема в том, что у меня работает код, однако PyCharm и mypy не знают тип a или b и говорят, что тип List[T], это имеет смысл, так как я не указал, что T есть.

Как сделать так, чтобы a имел тип List[str], а b имел тип List[int]?

_mock_a = list('abcdefghijklmnopqrstuvwxyz')
_mock_b = [int(i) for i in '12345678901234567890123456']

from typing import TypeVar, Callable, List

T = TypeVar('T')


class Base:
    def pipe(self, fn: Callable[['Base'], List[T]]) -> List[T]:
        return fn(self)


class A(Base):
    def get(self, index=0, count=5) -> List[str]:
        return _mock_a[index:index+count]

    def count(self) -> int:
        return len(_mock_a)


class B(Base):
    def get(self, index=0, count=5) -> List[int]:
        return _mock_b[index:index+count]

    def count(self) -> int:
        return len(_mock_b)


class C(Base):
    def other(self) -> None:
        pass


def get_all(base: Base) -> List[T]:
    step = 5
    return [
        item
        for start in range(0, base.count(), step)
        for item in base.get(start, step)
    ]


# Has type List[T], but I want it to have List[str]
a = A().pipe(get_all)
print(a)
# Has type List[T], but I want it to have List[int]
b = B().pipe(get_all)
print(b)

Я пытался исправить это, но ни один из них не работал.

class Method(Generic[T]):
    @staticmethod
    def get_all(base: Base) -> List[T]:
        step = 5
        return [
            item
            for start in range(0, base.count(), step)
            for item in base.get(start, step)
        ]


a = A().pipe(Method[str].get_all)
print(a)
class Base:
    def pipe(self, t: Type[T], fn: Callable[['Base'], T]) -> T:
        return fn(self)


a = A().pipe(List[str], get_all)
print(a)

Я нашел способ заставить работать вторую, которая работает как typing.cast:

class Base:
    def pipe(self, fn: Callable[['GetableEndpoint[T]'], List[T]], t: Type[T]=T) -> List[T]:
        return fn(cast(GetableEndpoint[T], self))


class GetableEndpoint(Generic[T], Base, metaclass=abc.ABCMeta):
    @classmethod
    def __subclasshook__(cls, C):
        if cls is GetableEndpoint:
            if any('get' in B.__dict__ for B in C.__mro__) and any('count' in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

    @abc.abstractmethod
    def get(self, index=0, count=5) -> List[T]:
        raise NotImplementedError()

    @abc.abstractmethod
    def count(self) -> int:
        raise NotImplementedError()


def get_all(base: GetableEndpoint[T]) -> List[T]:
    step = 5
    return [
        item
        for start in range(0, base.count(), step)
        for item in base.get(start, step)
    ]


a = A().pipe(get_all, str)

1 Ответ

0 голосов
/ 05 ноября 2018

Вопрос Аннотация типа Python для пользовательского типа утки похож на этот вопрос и включает ссылку на Протоколы (например, структурный подтип) . Эта проблема создала PEP 544 , которая имеет реализацию в typing_extensions.

Это означает, что для исправления вышеизложенного мы можем изменить GetableEndpoint на Protocol.

from typing import TypeVar, List
from typing_extensions import Protocol
import abc

T = TypeVar('T')


class GetableEndpoint(Protocol[T]):
    @abc.abstractmethod
    def get(self, index=0, count=5) -> List[T]:
        pass

    @abc.abstractmethod
    def count(self) -> int:
        pass

Это позволяет использовать в PyCharm и Mypy полностью типизированное значение:

class A(Base):
    def get(self, index=0, count=5) -> List[str]:
        return _mock_a[index:index+count]

    def count(self) -> int:
        return len(_mock_a)


def get_all(base: GetableEndpoint[T]) -> List[T]:
    step = 5
    return [
        item
        for start in range(0, base.count(), step)
        for item in base.get(start, step)
    ]


a = get_all(A())
print(a)

Я не могу заставить pipe работать, однако теперь у него есть полностью работающие типы, которые я считаю более важными.

...