Use ParamSpec to preserve function signatures of tasks #75
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Currently when you decorate a function as a task its signature is completely lost to typecheckers. This PR tries to improve on that with the help of
ParamSpec
(https://docs.python.org/3/library/typing.html#typing.ParamSpec, https://peps.python.org/pep-0612/). There are some caveats to this approach though, so if you decide that the downsides are bigger than the upsides and decline to merge this for that reason, that's okay. Let me cover the positives first and then the negatives.Say you have the following tasks:
With this PR, a typechecker can verify that the argument types are correct in these cases:
And wrong in these cases:
They can verify that the return types allow doing these operations:
And that they don't allow these:
They can verify that this is correct usage with
bind
:And that this is an incorrect call:
And that this is an incorrect definition:
Now for the caveats:
Conspicuously absent from my very first example were these:
Unfortunately we can't verify the argument types in these cases. The two first ones don't work because they don't take their arguments as
*args, **kwargs
, which is required forParamSpec
. The two latter don't work because you can pass partial arguments tos
and there's no way to represent a partialParamSpec
. In other words only direct calls anddelay
actually give you type-checked arguments.To pass around and store the type variables representing the signature I've had to make
Task
andSignature
generic. Since they're not generic at runtime, this is a bit misleading. Anyone who wants to annotate these with generics in their code will have to either usefrom __future__ import annotations
or wrap the type in quotes. Anyone who uses Mypy withdisallow_any_generics
and has previously annotated these without generics will be forced to specify the generics now, which I guess technically makes this a breaking change.There's also no attempt to support chains, groups, chords etc. I suspect it's not really possible with the current capabilities of the Python type system, but I haven't looked into it.