JulienPalard / Pipe

A Python library to use infix notation in Python
MIT License
1.96k stars 113 forks source link

Add type hints #76

Open devRMA opened 2 years ago

devRMA commented 2 years ago

Use a static type checker called mypy. In libs, I think it's very important to have type hints, so that when users are going to use the lib, code linters can help, knowing exactly what kind of parameters to receive, and what the return type is.

The ideal solution would be to add type hints as specified by PEP 484, PEP 526, PEP 544, PEP 586, PEP 589, and PEP 591 directly on your code.

devRMA commented 2 years ago

I even noticed that you had already added mypy to the requirements. Just need to do static typing.

https://github.com/JulienPalard/Pipe/blob/dd179c8ff0aa28ee0524f3247e5cb1c51347cba6/requirements.txt#L3-L4

JulienPalard commented 2 years ago

Probably blocked by https://github.com/python/mypy/issues/11833, as I'll need something like:

P = ParamSpec("P")
I = TypeVar("I")
O = TypeVar("O")

class Pipe(Generic[P, I, O]):
    def __init__(self, function: Callable[Concatenate[Iterable[I], P], Iterator[O]]) -> None:
        ...

as later the __call__ arguments are P. (The Iterable[I] being passed to the function via __ror__.)

But yes I agree it would be very nice to be able to write something like:

T = TypeVar("T")
take: Pipe[T, int, T]

to express than take takes an int, and yields the same type that the feeded type.

JulienPalard commented 2 years ago

Now blocked by mypy issue 1484 as we need a way to cleanly decribe functools.partial, to properly describe:

def __call__(self, *args, **kwargs):
    return Pipe(
        lambda iterable, *args2, **kwargs2: self.function(
            iterable, *args, *args2, **kwargs, **kwargs2
        )
    )
devRMA commented 2 years ago

It's more complex than I thought 😳

0xMochan commented 1 year ago

Now blocked by mypy issue 1484 as we need a way to cleanly decribe functools.partial, to properly describe:

def __call__(self, *args, **kwargs):
    return Pipe(
        lambda iterable, *args2, **kwargs2: self.function(
            iterable, *args, *args2, **kwargs, **kwargs2
        )
    )

I'm pretty sure this is nearly impossible without being able to represent HKTs (Higher-Kinded Types) in the Python type system.

fabiob commented 8 months ago

@JulienPalard and @devRMA, would you share the code you write so far in a branch or fork? I would love to take a swing at it and try to help you.

JulienPalard commented 8 months ago

@fabiob pushed my typing branch, see the pyi file. I know it's not much and does not works.

What we really need is a clean way to type functools.partial, as it's what's pipe does a lot.

p-98 commented 5 months ago

Seems like https://github.com/python/mypy/issues/1484 was completed with https://github.com/python/mypy/pull/16939.

Does the implementation provide what you need?

JulienPalard commented 5 months ago

I don't think so, they implemented a mypy plugin to handle the single functools.partial function, I really mean there's an actual if fullname == "functools.partial": in the implementation.

Looks like there's still no way to type annotate functools.partial equivalents.

JulienPalard commented 5 months ago

Currently this works:

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

P = ParamSpec('P')
T = TypeVar('T')
U = TypeVar('U')

class Pipe(Generic[U, P, T]):
    def __init__(self, function: Callable[Concatenate[U, P], T]):
        self.function = function

    def run(self, arg1: U) -> T:
        return self.function(arg1)

def simple_annotated_function(value: int) -> str:
    assert isinstance(value, int)
    return "hello"

def main() -> None:
    p = Pipe(simple_annotated_function)
    reveal_type(p)  # demo.Pipe[builtins.int, [], builtins.str]
    reveal_type(p.function) # def (builtins.int) -> builtins.str
    reveal_type(p.run) # def (arg1: builtins.int) -> builtins.str
    p.run("coucou") # Argument 1 to "run" of "Pipe" has incompatible type "str"; expected "int"

main()

But it feels like it's only 1/10 of the work, as we need some way to "substract" parameters from a paramspec.

JulienPalard commented 5 months ago

Starting a new discussion here: https://github.com/python/mypy/issues/17481 to track this.