Skip to content

Commit 99644d6

Browse files
committed
Merge branch 'main' into dcreager/overloads
* main: [red-knot] Understand `typing.Callable` (#16493) [red-knot] Support unpacking `with` target (#16469) [red-knot] Attribute access and the descriptor protocol (#16416) [`pep8-naming`] Add links to `ignore-names` options in various rules' documentation (#16557) [red-knot] avoid inferring types if unpacking fails (#16530)
2 parents 48b2b56 + 0361021 commit 99644d6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+3363
-896
lines changed

crates/red_knot_project/tests/check.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,24 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
216216
self.visit_body(&for_stmt.orelse);
217217
return;
218218
}
219+
Stmt::With(with_stmt) => {
220+
for item in &with_stmt.items {
221+
if let Some(target) = &item.optional_vars {
222+
self.visit_target(target);
223+
}
224+
self.visit_expr(&item.context_expr);
225+
}
226+
227+
self.visit_body(&with_stmt.body);
228+
return;
229+
}
219230
Stmt::AnnAssign(_)
220231
| Stmt::Return(_)
221232
| Stmt::Delete(_)
222233
| Stmt::AugAssign(_)
223234
| Stmt::TypeAlias(_)
224235
| Stmt::While(_)
225236
| Stmt::If(_)
226-
| Stmt::With(_)
227237
| Stmt::Match(_)
228238
| Stmt::Raise(_)
229239
| Stmt::Try(_)
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Callable
2+
3+
References:
4+
5+
- <https://typing.readthedocs.io/en/latest/spec/callables.html#callable>
6+
7+
TODO: Use `collections.abc` as importing from `typing` is deprecated but this requires support for
8+
`*` imports. See: <https://docs.python.org/3/library/typing.html#deprecated-aliases>.
9+
10+
## Invalid forms
11+
12+
The `Callable` special form requires _exactly_ two arguments where the first argument is either a
13+
parameter type list, parameter specification, `typing.Concatenate`, or `...` and the second argument
14+
is the return type. Here, we explore various invalid forms.
15+
16+
### Empty
17+
18+
A bare `Callable` without any type arguments:
19+
20+
```py
21+
from typing import Callable
22+
23+
def _(c: Callable):
24+
reveal_type(c) # revealed: (...) -> Unknown
25+
```
26+
27+
### Invalid parameter type argument
28+
29+
When it's not a list:
30+
31+
```py
32+
from typing import Callable
33+
34+
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
35+
def _(c: Callable[int, str]):
36+
reveal_type(c) # revealed: (...) -> Unknown
37+
```
38+
39+
Or, when it's a literal type:
40+
41+
```py
42+
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
43+
def _(c: Callable[42, str]):
44+
reveal_type(c) # revealed: (...) -> Unknown
45+
```
46+
47+
Or, when one of the parameter type is invalid in the list:
48+
49+
```py
50+
def _(c: Callable[[int, 42, str, False], None]):
51+
# revealed: (int, @Todo(number literal in type expression), str, @Todo(boolean literal in type expression), /) -> None
52+
reveal_type(c)
53+
```
54+
55+
### Missing return type
56+
57+
Using a parameter list:
58+
59+
```py
60+
from typing import Callable
61+
62+
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
63+
def _(c: Callable[[int, str]]):
64+
reveal_type(c) # revealed: (int, str, /) -> Unknown
65+
```
66+
67+
Or, an ellipsis:
68+
69+
```py
70+
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
71+
def _(c: Callable[...]):
72+
reveal_type(c) # revealed: (...) -> Unknown
73+
```
74+
75+
### More than two arguments
76+
77+
We can't reliably infer the callable type if there are more then 2 arguments because we don't know
78+
which argument corresponds to either the parameters or the return type.
79+
80+
```py
81+
from typing import Callable
82+
83+
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
84+
def _(c: Callable[[int], str, str]):
85+
reveal_type(c) # revealed: (...) -> Unknown
86+
```
87+
88+
## Simple
89+
90+
A simple `Callable` with multiple parameters and a return type:
91+
92+
```py
93+
from typing import Callable
94+
95+
def _(c: Callable[[int, str], int]):
96+
reveal_type(c) # revealed: (int, str, /) -> int
97+
```
98+
99+
## Nested
100+
101+
A nested `Callable` as one of the parameter types:
102+
103+
```py
104+
from typing import Callable
105+
106+
def _(c: Callable[[Callable[[int], str]], int]):
107+
reveal_type(c) # revealed: ((int, /) -> str, /) -> int
108+
```
109+
110+
And, as the return type:
111+
112+
```py
113+
def _(c: Callable[[int, str], Callable[[int], int]]):
114+
reveal_type(c) # revealed: (int, str, /) -> (int, /) -> int
115+
```
116+
117+
## Gradual form
118+
119+
The `Callable` special form supports the use of `...` in place of the list of parameter types. This
120+
is a [gradual form] indicating that the type is consistent with any input signature:
121+
122+
```py
123+
from typing import Callable
124+
125+
def gradual_form(c: Callable[..., str]):
126+
reveal_type(c) # revealed: (...) -> str
127+
```
128+
129+
## Using `typing.Concatenate`
130+
131+
Using `Concatenate` as the first argument to `Callable`:
132+
133+
```py
134+
from typing_extensions import Callable, Concatenate
135+
136+
def _(c: Callable[Concatenate[int, str, ...], int]):
137+
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
138+
```
139+
140+
And, as one of the parameter types:
141+
142+
```py
143+
def _(c: Callable[[Concatenate[int, str, ...], int], int]):
144+
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
145+
```
146+
147+
## Using `typing.ParamSpec`
148+
149+
Using a `ParamSpec` in a `Callable` annotation:
150+
151+
```py
152+
from typing_extensions import Callable
153+
154+
# TODO: Not an error; remove once `ParamSpec` is supported
155+
# error: [invalid-type-form]
156+
def _[**P1](c: Callable[P1, int]):
157+
reveal_type(c) # revealed: (...) -> Unknown
158+
```
159+
160+
And, using the legacy syntax:
161+
162+
```py
163+
from typing_extensions import ParamSpec
164+
165+
P2 = ParamSpec("P2")
166+
167+
# TODO: Not an error; remove once `ParamSpec` is supported
168+
# error: [invalid-type-form]
169+
def _(c: Callable[P2, int]):
170+
reveal_type(c) # revealed: (...) -> Unknown
171+
```
172+
173+
## Using `typing.Unpack`
174+
175+
Using the unpack operator (`*`):
176+
177+
```py
178+
from typing_extensions import Callable, TypeVarTuple
179+
180+
Ts = TypeVarTuple("Ts")
181+
182+
def _(c: Callable[[int, *Ts], int]):
183+
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
184+
```
185+
186+
And, using the legacy syntax using `Unpack`:
187+
188+
```py
189+
from typing_extensions import Unpack
190+
191+
def _(c: Callable[[int, Unpack[Ts]], int]):
192+
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
193+
```
194+
195+
[gradual form]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-gradual-form

crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,12 @@ qux = (foo, bar)
7373
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
7474

7575
# TODO: Infer "LiteralString"
76-
reveal_type(foo.join(qux)) # revealed: @Todo(overloaded method)
76+
reveal_type(foo.join(qux)) # revealed: @Todo(return type of decorated function)
7777

7878
template: LiteralString = "{}, {}"
7979
reveal_type(template) # revealed: Literal["{}, {}"]
8080
# TODO: Infer `LiteralString`
81-
reveal_type(template.format(foo, bar)) # revealed: @Todo(overloaded method)
81+
reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of decorated function)
8282
```
8383

8484
### Assignability

crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ import typing
7070

7171
class ListSubclass(typing.List): ...
7272

73-
# TODO: should have `Generic`, should not have `Unknown`
74-
# revealed: tuple[Literal[ListSubclass], Literal[list], Unknown, Literal[object]]
73+
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
7574
reveal_type(ListSubclass.__mro__)
7675

7776
class DictSubclass(typing.Dict): ...

crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
2929
# TODO: should understand the annotation
3030
reveal_type(kwargs) # revealed: dict
3131

32+
# TODO: not an error; remove once `call` is implemented for `Callable`
33+
# error: [call-non-callable]
3234
return callback(42, *args, **kwargs)
3335

3436
class Foo:

crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ def _(flag: bool):
7575

7676
f = Foo()
7777

78-
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
79-
# that `Foo.__iadd__` may be unbound as additional context.
78+
# error: [unsupported-operator] "Operator `+=` is unsupported between objects of type `Foo` and `Literal["Hello, world!"]`"
8079
f += "Hello, world!"
8180

8281
reveal_type(f) # revealed: int | Unknown

0 commit comments

Comments
 (0)