## Summary Part of https://github.com/astral-sh/ruff/issues/15382 This PR implements a general callable type that wraps around a `Signature` and it uses that new type to represent `typing.Callable`. It also implements `Display` support for `Callable`. The format is as: ``` ([<arg name>][: <arg type>][ = <default type>], ...) -> <return type> ``` The `/` and `*` separators are added at the correct boundary for positional-only and keyword-only parameters. Now, as `typing.Callable` only has positional-only parameters, the rendered signature would be: ```py Callable[[int, str], None] # (int, str, /) -> None ``` The `/` separator represents that all the arguments are positional-only. The relationship methods that check assignability, subtype relationship, etc. are not yet implemented and will be done so as a follow-up. ## Test Plan Add test cases for display support for `Signature` and various mdtest for `typing.Callable`.
5.1 KiB
Callable
References:
TODO: Use collections.abc as importing from typing is deprecated but this requires support for
* imports. See: https://docs.python.org/3/library/typing.html#deprecated-aliases.
Invalid forms
The Callable special form requires exactly two arguments where the first argument is either a
parameter type list, parameter specification, typing.Concatenate, or ... and the second argument
is the return type. Here, we explore various invalid forms.
Empty
A bare Callable without any type arguments:
from typing import Callable
def _(c: Callable):
reveal_type(c) # revealed: (...) -> Unknown
Invalid parameter type argument
When it's not a list:
from typing import Callable
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
def _(c: Callable[int, str]):
reveal_type(c) # revealed: (...) -> Unknown
Or, when it's a literal type:
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
def _(c: Callable[42, str]):
reveal_type(c) # revealed: (...) -> Unknown
Or, when one of the parameter type is invalid in the list:
def _(c: Callable[[int, 42, str, False], None]):
# revealed: (int, @Todo(number literal in type expression), str, @Todo(boolean literal in type expression), /) -> None
reveal_type(c)
Missing return type
Using a parameter list:
from typing import Callable
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
def _(c: Callable[[int, str]]):
reveal_type(c) # revealed: (int, str, /) -> Unknown
Or, an ellipsis:
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
def _(c: Callable[...]):
reveal_type(c) # revealed: (...) -> Unknown
More than two arguments
We can't reliably infer the callable type if there are more then 2 arguments because we don't know which argument corresponds to either the parameters or the return type.
from typing import Callable
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
def _(c: Callable[[int], str, str]):
reveal_type(c) # revealed: (...) -> Unknown
Simple
A simple Callable with multiple parameters and a return type:
from typing import Callable
def _(c: Callable[[int, str], int]):
reveal_type(c) # revealed: (int, str, /) -> int
Nested
A nested Callable as one of the parameter types:
from typing import Callable
def _(c: Callable[[Callable[[int], str]], int]):
reveal_type(c) # revealed: ((int, /) -> str, /) -> int
And, as the return type:
def _(c: Callable[[int, str], Callable[[int], int]]):
reveal_type(c) # revealed: (int, str, /) -> (int, /) -> int
Gradual form
The Callable special form supports the use of ... in place of the list of parameter types. This
is a gradual form indicating that the type is consistent with any input signature:
from typing import Callable
def gradual_form(c: Callable[..., str]):
reveal_type(c) # revealed: (...) -> str
Using typing.Concatenate
Using Concatenate as the first argument to Callable:
from typing_extensions import Callable, Concatenate
def _(c: Callable[Concatenate[int, str, ...], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
And, as one of the parameter types:
def _(c: Callable[[Concatenate[int, str, ...], int], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
Using typing.ParamSpec
Using a ParamSpec in a Callable annotation:
from typing_extensions import Callable
# TODO: Not an error; remove once `ParamSpec` is supported
# error: [invalid-type-form]
def _[**P1](c: Callable[P1, int]):
reveal_type(c) # revealed: (...) -> Unknown
And, using the legacy syntax:
from typing_extensions import ParamSpec
P2 = ParamSpec("P2")
# TODO: Not an error; remove once `ParamSpec` is supported
# error: [invalid-type-form]
def _(c: Callable[P2, int]):
reveal_type(c) # revealed: (...) -> Unknown
Using typing.Unpack
Using the unpack operator (*):
from typing_extensions import Callable, TypeVarTuple
Ts = TypeVarTuple("Ts")
def _(c: Callable[[int, *Ts], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
And, using the legacy syntax using Unpack:
from typing_extensions import Unpack
def _(c: Callable[[int, Unpack[Ts]], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int