Skip to content

Commit 9305233

Browse files
authored
[red-knot] Allow CallableTypeFromFunction to display the signatures of callable types that are not function literals (astral-sh#17047)
I found this helpful for understanding some of the stuff that was going on in astral-sh#17005.
1 parent e07741e commit 9305233

File tree

12 files changed

+212
-189
lines changed

12 files changed

+212
-189
lines changed

crates/red_knot_python_semantic/resources/mdtest/type_api.md

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -398,16 +398,16 @@ def f(x: TypeOf) -> None:
398398
reveal_type(x) # revealed: Unknown
399399
```
400400

401-
## `CallableTypeFromFunction`
401+
## `CallableTypeOf`
402402

403-
The `CallableTypeFromFunction` special form can be used to extract the type of a function literal as
404-
a callable type. This can be used to get the externally-visibly signature of the function, which can
405-
then be used to test various type properties.
403+
The `CallableTypeOf` special form can be used to extract the `Callable` structural type inhabited by
404+
a given callable object. This can be used to get the externally visibly signature of the object,
405+
which can then be used to test various type properties.
406406

407-
It accepts a single type parameter which is expected to be a function literal.
407+
It accepts a single type parameter which is expected to be a callable object.
408408

409409
```py
410-
from knot_extensions import CallableTypeFromFunction
410+
from knot_extensions import CallableTypeOf
411411

412412
def f1():
413413
return
@@ -418,25 +418,41 @@ def f2() -> int:
418418
def f3(x: int, y: str) -> None:
419419
return
420420

421-
# error: [invalid-type-form] "Special form `knot_extensions.CallableTypeFromFunction` expected exactly one type parameter"
422-
c1: CallableTypeFromFunction[f1, f2]
423-
# error: [invalid-type-form] "Expected the first argument to `knot_extensions.CallableTypeFromFunction` to be a function literal, but got `Literal[int]`"
424-
c2: CallableTypeFromFunction[int]
421+
# error: [invalid-type-form] "Special form `knot_extensions.CallableTypeOf` expected exactly one type parameter"
422+
c1: CallableTypeOf[f1, f2]
425423

426-
# error: [invalid-type-form] "`knot_extensions.CallableTypeFromFunction` requires exactly one argument when used in a type expression"
427-
def f(x: CallableTypeFromFunction) -> None:
424+
# error: [invalid-type-form] "Expected the first argument to `knot_extensions.CallableTypeOf` to be a callable object, but got an object of type `Literal["foo"]`"
425+
c2: CallableTypeOf["foo"]
426+
427+
# error: [invalid-type-form] "`knot_extensions.CallableTypeOf` requires exactly one argument when used in a type expression"
428+
def f(x: CallableTypeOf) -> None:
428429
reveal_type(x) # revealed: Unknown
429430
```
430431

431-
Using it in annotation to reveal the signature of the function:
432+
Using it in annotation to reveal the signature of the callable object:
432433

433434
```py
435+
class Foo:
436+
def __init__(self, x: int) -> None:
437+
pass
438+
439+
def __call__(self, x: int) -> str:
440+
return "foo"
441+
434442
def _(
435-
c1: CallableTypeFromFunction[f1],
436-
c2: CallableTypeFromFunction[f2],
437-
c3: CallableTypeFromFunction[f3],
443+
c1: CallableTypeOf[f1],
444+
c2: CallableTypeOf[f2],
445+
c3: CallableTypeOf[f3],
446+
c4: CallableTypeOf[Foo],
447+
c5: CallableTypeOf[Foo(42).__call__],
438448
) -> None:
439449
reveal_type(c1) # revealed: () -> Unknown
440450
reveal_type(c2) # revealed: () -> int
441451
reveal_type(c3) # revealed: (x: int, y: str) -> None
452+
453+
# TODO: should be `(x: int) -> Foo`
454+
reveal_type(c4) # revealed: (...) -> Foo
455+
456+
# TODO: `self` is bound here; this should probably be `(x: int) -> str`?
457+
reveal_type(c5) # revealed: (self, x: int) -> str
442458
```

crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ are covered in the [subtyping tests](./is_subtype_of.md#callable).
402402
### Return type
403403

404404
```py
405-
from knot_extensions import CallableTypeFromFunction, Unknown, static_assert, is_assignable_to
405+
from knot_extensions import CallableTypeOf, Unknown, static_assert, is_assignable_to
406406
from typing import Any, Callable
407407

408408
static_assert(is_assignable_to(Callable[[], Any], Callable[[], int]))
@@ -432,7 +432,7 @@ A `Callable` which uses the gradual form (`...`) for the parameter types is cons
432432
input signature.
433433

434434
```py
435-
from knot_extensions import CallableTypeFromFunction, static_assert, is_assignable_to
435+
from knot_extensions import CallableTypeOf, static_assert, is_assignable_to
436436
from typing import Any, Callable
437437

438438
static_assert(is_assignable_to(Callable[[], None], Callable[..., None]))
@@ -450,12 +450,12 @@ def keyword_only(*, a: int, b: int) -> None: ...
450450
def keyword_variadic(**kwargs: int) -> None: ...
451451
def mixed(a: int, /, b: int, *args: int, c: int, **kwargs: int) -> None: ...
452452

453-
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
454-
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
455-
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
456-
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
457-
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
458-
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
453+
static_assert(is_assignable_to(CallableTypeOf[positional_only], Callable[..., None]))
454+
static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None]))
455+
static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None]))
456+
static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None]))
457+
static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None]))
458+
static_assert(is_assignable_to(CallableTypeOf[mixed], Callable[..., None]))
459459
```
460460

461461
And, even if the parameters are unannotated.
@@ -468,12 +468,12 @@ def keyword_only(*, a, b) -> None: ...
468468
def keyword_variadic(**kwargs) -> None: ...
469469
def mixed(a, /, b, *args, c, **kwargs) -> None: ...
470470

471-
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
472-
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
473-
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
474-
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
475-
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
476-
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
471+
static_assert(is_assignable_to(CallableTypeOf[positional_only], Callable[..., None]))
472+
static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None]))
473+
static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None]))
474+
static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None]))
475+
static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None]))
476+
static_assert(is_assignable_to(CallableTypeOf[mixed], Callable[..., None]))
477477
```
478478

479479
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation

crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ the parameter in one of the callable has a default value then the corresponding
127127
other callable should also have a default value.
128128

129129
```py
130-
from knot_extensions import CallableTypeFromFunction, is_equivalent_to, static_assert
130+
from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert
131131
from typing import Callable
132132

133133
def f1(a: int = 1) -> None: ...
134134
def f2(a: int = 2) -> None: ...
135135

136-
static_assert(is_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
136+
static_assert(is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
137137
```
138138

139139
The names of the positional-only, variadic and keyword-variadic parameters does not need to be the
@@ -143,7 +143,7 @@ same.
143143
def f3(a1: int, /, *args1: int, **kwargs2: int) -> None: ...
144144
def f4(a2: int, /, *args2: int, **kwargs1: int) -> None: ...
145145

146-
static_assert(is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
146+
static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
147147
```
148148

149149
Putting it all together, the following two callables are equivalent:
@@ -152,15 +152,15 @@ Putting it all together, the following two callables are equivalent:
152152
def f5(a1: int, /, b: float, c: bool = False, *args1: int, d: int = 1, e: str, **kwargs1: float) -> None: ...
153153
def f6(a2: int, /, b: float, c: bool = True, *args2: int, d: int = 2, e: str, **kwargs2: float) -> None: ...
154154

155-
static_assert(is_equivalent_to(CallableTypeFromFunction[f5], CallableTypeFromFunction[f6]))
155+
static_assert(is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6]))
156156
```
157157

158158
### Not equivalent
159159

160160
There are multiple cases when two callable types are not equivalent which are enumerated below.
161161

162162
```py
163-
from knot_extensions import CallableTypeFromFunction, is_equivalent_to, static_assert
163+
from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert
164164
from typing import Callable
165165
```
166166

@@ -170,7 +170,7 @@ When the number of parameters is different:
170170
def f1(a: int) -> None: ...
171171
def f2(a: int, b: int) -> None: ...
172172

173-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
173+
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
174174
```
175175

176176
When either of the callable types uses a gradual form for the parameters:
@@ -187,9 +187,9 @@ def f3(): ...
187187
def f4() -> None: ...
188188

189189
static_assert(not is_equivalent_to(Callable[[], int], Callable[[], None]))
190-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f3]))
191-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
192-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f4], CallableTypeFromFunction[f3]))
190+
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3]))
191+
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
192+
static_assert(not is_equivalent_to(CallableTypeOf[f4], CallableTypeOf[f3]))
193193
```
194194

195195
When the parameter names are different:
@@ -198,13 +198,13 @@ When the parameter names are different:
198198
def f5(a: int) -> None: ...
199199
def f6(b: int) -> None: ...
200200

201-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f5], CallableTypeFromFunction[f6]))
201+
static_assert(not is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6]))
202202
```
203203

204204
When only one of the callable types has parameter names:
205205

206206
```py
207-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f5], Callable[[int], None]))
207+
static_assert(not is_equivalent_to(CallableTypeOf[f5], Callable[[int], None]))
208208
```
209209

210210
When the parameter kinds are different:
@@ -213,7 +213,7 @@ When the parameter kinds are different:
213213
def f7(a: int, /) -> None: ...
214214
def f8(a: int) -> None: ...
215215

216-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f7], CallableTypeFromFunction[f8]))
216+
static_assert(not is_equivalent_to(CallableTypeOf[f7], CallableTypeOf[f8]))
217217
```
218218

219219
When the annotated types of the parameters are not equivalent or absent in one or both of the
@@ -224,10 +224,10 @@ def f9(a: int) -> None: ...
224224
def f10(a: str) -> None: ...
225225
def f11(a) -> None: ...
226226

227-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f9], CallableTypeFromFunction[f10]))
228-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f10], CallableTypeFromFunction[f11]))
229-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f11], CallableTypeFromFunction[f10]))
230-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f11], CallableTypeFromFunction[f11]))
227+
static_assert(not is_equivalent_to(CallableTypeOf[f9], CallableTypeOf[f10]))
228+
static_assert(not is_equivalent_to(CallableTypeOf[f10], CallableTypeOf[f11]))
229+
static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f10]))
230+
static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f11]))
231231
```
232232

233233
When the default value for a parameter is present only in one of the callable type:
@@ -236,8 +236,8 @@ When the default value for a parameter is present only in one of the callable ty
236236
def f12(a: int) -> None: ...
237237
def f13(a: int = 2) -> None: ...
238238

239-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f12], CallableTypeFromFunction[f13]))
240-
static_assert(not is_equivalent_to(CallableTypeFromFunction[f13], CallableTypeFromFunction[f12]))
239+
static_assert(not is_equivalent_to(CallableTypeOf[f12], CallableTypeOf[f13]))
240+
static_assert(not is_equivalent_to(CallableTypeOf[f13], CallableTypeOf[f12]))
241241
```
242242

243243
[the equivalence relation]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent

crates/red_knot_python_semantic/resources/mdtest/type_properties/is_fully_static.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Using function literals, we can check more variations of callable types as it al
8080
parameters without annotations and no return type.
8181

8282
```py
83-
from knot_extensions import CallableTypeFromFunction, is_fully_static, static_assert
83+
from knot_extensions import CallableTypeOf, is_fully_static, static_assert
8484

8585
def f00() -> None: ...
8686
def f01(a: int, b: str) -> None: ...
@@ -90,12 +90,12 @@ def f13(a, b: int): ...
9090
def f14(a, b: int) -> None: ...
9191
def f15(a, b) -> None: ...
9292

93-
static_assert(is_fully_static(CallableTypeFromFunction[f00]))
94-
static_assert(is_fully_static(CallableTypeFromFunction[f01]))
93+
static_assert(is_fully_static(CallableTypeOf[f00]))
94+
static_assert(is_fully_static(CallableTypeOf[f01]))
9595

96-
static_assert(not is_fully_static(CallableTypeFromFunction[f11]))
97-
static_assert(not is_fully_static(CallableTypeFromFunction[f12]))
98-
static_assert(not is_fully_static(CallableTypeFromFunction[f13]))
99-
static_assert(not is_fully_static(CallableTypeFromFunction[f14]))
100-
static_assert(not is_fully_static(CallableTypeFromFunction[f15]))
96+
static_assert(not is_fully_static(CallableTypeOf[f11]))
97+
static_assert(not is_fully_static(CallableTypeOf[f12]))
98+
static_assert(not is_fully_static(CallableTypeOf[f13]))
99+
static_assert(not is_fully_static(CallableTypeOf[f14]))
100+
static_assert(not is_fully_static(CallableTypeOf[f15]))
101101
```

crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ gradual types. The cases with fully static types and using different combination
7373
are covered in the [equivalence tests](./is_equivalent_to.md#callable).
7474

7575
```py
76-
from knot_extensions import Unknown, CallableTypeFromFunction, is_gradual_equivalent_to, static_assert
76+
from knot_extensions import Unknown, CallableTypeOf, is_gradual_equivalent_to, static_assert
7777
from typing import Any, Callable
7878

7979
static_assert(is_gradual_equivalent_to(Callable[..., int], Callable[..., int]))
@@ -92,7 +92,7 @@ type of `Any`.
9292
def f1():
9393
return
9494

95-
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f1], Callable[[], Any]))
95+
static_assert(is_gradual_equivalent_to(CallableTypeOf[f1], Callable[[], Any]))
9696
```
9797

9898
And, similarly for parameters with no annotations.
@@ -101,7 +101,7 @@ And, similarly for parameters with no annotations.
101101
def f2(a, b, /) -> None:
102102
return
103103

104-
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f2], Callable[[Any, Any], None]))
104+
static_assert(is_gradual_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None]))
105105
```
106106

107107
Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs`
@@ -115,8 +115,8 @@ def variadic_without_annotation(*args, **kwargs):
115115
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
116116
return
117117

118-
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_without_annotation], Callable[..., Any]))
119-
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_with_annotation], Callable[..., Any]))
118+
static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any]))
119+
static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any]))
120120
```
121121

122122
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
@@ -129,8 +129,8 @@ def variadic_args(*args):
129129
def variadic_kwargs(**kwargs):
130130
return
131131

132-
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_args], Callable[..., Any]))
133-
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_kwargs], Callable[..., Any]))
132+
static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any]))
133+
static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any]))
134134
```
135135

136136
Parameter names, default values, and it's kind should also be considered when checking for gradual
@@ -140,18 +140,18 @@ equivalence.
140140
def f1(a): ...
141141
def f2(b): ...
142142

143-
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
143+
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
144144

145145
def f3(a=1): ...
146146
def f4(a=2): ...
147147
def f5(a): ...
148148

149-
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
150-
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f5]))
149+
static_assert(is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
150+
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5]))
151151

152152
def f6(a, /): ...
153153

154-
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f6]))
154+
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
155155
```
156156

157157
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize

0 commit comments

Comments
 (0)