Skip to content

Commit 0086737

Browse files
committed
[ty] Add partial support for TypeIs
1 parent e293411 commit 0086737

File tree

10 files changed

+609
-54
lines changed

10 files changed

+609
-54
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
1919
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
2020

2121
def g() -> TypeGuard[int]: ...
22-
def h() -> TypeIs[int]: ...
2322
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
2423
reveal_type(args) # revealed: tuple[@Todo(Support for `typing.ParamSpec`), ...]
2524
reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)]
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# User-defined type guards
2+
3+
User-defined type guards are functions of which the return type is either `TypeGuard[...]` or
4+
`TypeIs[...]`.
5+
6+
## Display
7+
8+
```py
9+
from ty_extensions import Intersection, Not, TypeOf
10+
from typing_extensions import TypeGuard, TypeIs
11+
12+
def _(
13+
a: TypeGuard[str],
14+
b: TypeIs[str | int],
15+
c: TypeGuard[Intersection[complex, Not[int], Not[float]]],
16+
d: TypeIs[tuple[TypeOf[bytes]]],
17+
):
18+
# TODO: Should be `TypeGuard[str]`
19+
reveal_type(a) # revealed: @Todo(`TypeGuard[]` special form)
20+
reveal_type(b) # revealed: TypeIs[str | int]
21+
# TODO: Should be `TypeGuard[complex & ~int & ~float]`
22+
reveal_type(c) # revealed: @Todo(`TypeGuard[]` special form)
23+
reveal_type(d) # revealed: TypeIs[tuple[<class 'bytes'>]]
24+
25+
# TODO: error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeGuard[str]`"
26+
def _(a) -> TypeGuard[str]: ...
27+
# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`"
28+
def _(a) -> TypeIs[str]: ...
29+
30+
def f(a) -> TypeGuard[str]: return True
31+
def g(a) -> TypeIs[str]: return True
32+
def _(a: object):
33+
# TODO: Should be `TypeGuard[a, str]`
34+
reveal_type(f(a)) # revealed: @Todo(`TypeGuard[]` special form)
35+
reveal_type(g(a)) # revealed: TypeIs[a, str]
36+
```
37+
38+
## Parameters
39+
40+
A user-defined type guard must accept at least one positional argument (in addition to `self`/`cls`
41+
for non-static methods).
42+
43+
```pyi
44+
from typing_extensions import TypeGuard, TypeIs
45+
46+
# TODO: error: [invalid-type-guard-definition]
47+
def _() -> TypeGuard[str]: ...
48+
49+
# TODO: error: [invalid-type-guard-definition]
50+
def _(**kwargs) -> TypeIs[str]: ...
51+
52+
class _:
53+
# fine
54+
def _(self, /, a) -> TypeGuard[str]: ...
55+
@classmethod
56+
def _(cls, a) -> TypeGuard[str]: ...
57+
@staticmethod
58+
def _(a) -> TypeIs[str]: ...
59+
60+
# errors
61+
def _(self) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
62+
def _(self, /, *, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
63+
@classmethod
64+
def _(cls) -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
65+
@classmethod
66+
def _() -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
67+
@staticmethod
68+
def _(*, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
69+
```
70+
71+
For `TypeIs` functions, the narrowed type must be assignable to the declared type of that parameter,
72+
if any.
73+
74+
```pyi
75+
from typing import Any
76+
from typing_extensions import TypeIs
77+
78+
def _(a: object) -> TypeIs[str]: ...
79+
def _(a: Any) -> TypeIs[str]: ...
80+
def _(a: tuple[object]) -> TypeIs[tuple[str]]: ...
81+
def _(a: str | Any) -> TypeIs[str]: ...
82+
def _(a) -> TypeIs[str]: ...
83+
84+
# TODO: error: [invalid-type-guard-definition]
85+
def _(a: int) -> TypeIs[str]: ...
86+
87+
# TODO: error: [invalid-type-guard-definition]
88+
def _(a: bool | str) -> TypeIs[int]: ...
89+
```
90+
91+
## Arguments to special forms
92+
93+
`TypeGuard` and `TypeIs` accept exactly one type argument.
94+
95+
```py
96+
from typing_extensions import TypeGuard, TypeIs
97+
98+
a = 123
99+
100+
# TODO: error: [invalid-type-form]
101+
def f(_) -> TypeGuard[int, str]: ...
102+
103+
# error: [invalid-type-form]
104+
def g(_) -> TypeIs[a, str]: ...
105+
106+
# TODO: Should be `Unknown`
107+
reveal_type(f(0)) # revealed: @Todo(`TypeGuard[]` special form)
108+
reveal_type(g(0)) # revealed: Unknown
109+
```
110+
111+
## Return types
112+
113+
All code paths in a type guard function must return booleans.
114+
115+
```py
116+
from typing_extensions import Literal, TypeGuard, TypeIs, assert_never
117+
118+
def _(a: object, flag: bool) -> TypeGuard[str]:
119+
if flag:
120+
return 0
121+
122+
return "foo"
123+
124+
# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`"
125+
def f(a: object, flag: bool) -> TypeIs[str]:
126+
if flag:
127+
# error: [invalid-return-type] "Return type does not match returned value: expected `TypeIs[str]`, found `float`"
128+
return 1.2
129+
130+
def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]:
131+
if a == "foo":
132+
# Logically wrong, but allowed regardless
133+
return False
134+
135+
return False
136+
```
137+
138+
## Invalid calls
139+
140+
```pyi
141+
from typing import Any
142+
from typing_extensions import TypeGuard, TypeIs
143+
144+
def f(a: object) -> TypeGuard[str]: ...
145+
def g(a: object) -> TypeIs[int]: ...
146+
def _(d: Any):
147+
if f(): # error: [missing-argument]
148+
...
149+
150+
# TODO: Is this error correct?
151+
if g(*d): # error: [missing-argument]
152+
...
153+
154+
if f("foo"): # TODO: error: [invalid-type-guard-call]
155+
...
156+
157+
if g(a=d): # error: [invalid-type-guard-call]
158+
...
159+
160+
def _(a: tuple[str, int] | tuple[int, str]):
161+
if g(a[0]): # error: [invalid-type-guard-call]
162+
# TODO: Should be `tuple[str, int]`
163+
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
164+
```
165+
166+
## Narrowing
167+
168+
```py
169+
from typing import Any
170+
from typing_extensions import TypeGuard, TypeIs
171+
172+
def guard_str(a: object) -> TypeGuard[str]: return True
173+
def is_int(a: object) -> TypeIs[int]: return True
174+
def _(a: str | int):
175+
if guard_str(a):
176+
# TODO: Should be `str`
177+
reveal_type(a) # revealed: str | int
178+
else:
179+
reveal_type(a) # revealed: str | int
180+
181+
if is_int(a):
182+
reveal_type(a) # revealed: int
183+
else:
184+
reveal_type(a) # revealed: str & ~int
185+
186+
def _(a: str | int):
187+
b = guard_str(a)
188+
c = is_int(a)
189+
190+
reveal_type(a) # revealed: str | int
191+
# TODO: Should be `TypeGuard[a, str]`
192+
reveal_type(b) # revealed: @Todo(`TypeGuard[]` special form)
193+
reveal_type(c) # revealed: TypeIs[a, int]
194+
195+
if b:
196+
# TODO: Should be `str`
197+
reveal_type(a) # revealed: str | int
198+
else:
199+
reveal_type(a) # revealed: str | int
200+
201+
if c:
202+
reveal_type(a) # revealed: int
203+
else:
204+
reveal_type(a) # revealed: str & ~int
205+
206+
def _(x: str | int, flag: bool) -> None:
207+
b = is_int(x)
208+
reveal_type(b) # revealed: TypeIs[x, int]
209+
210+
if flag:
211+
x = ""
212+
213+
if b:
214+
# TODO: Should be `str | int`
215+
reveal_type(x) # revealed: int
216+
```
217+
218+
## `TypeGuard` special cases
219+
220+
```py
221+
from typing import Any
222+
from typing_extensions import TypeGuard
223+
224+
def guard_int(a: object) -> TypeGuard[int]: return True
225+
def is_int(a: object) -> TypeGuard[int]: return True
226+
def does_not_narrow_in_negative_case(a: str | int):
227+
if not guard_int(a):
228+
# TODO: Should be `str`
229+
reveal_type(a) # revealed: str | int
230+
else:
231+
reveal_type(a) # revealed: str | int
232+
233+
def narrowed_type_must_be_exact(a: object, b: bool):
234+
if guard_int(b):
235+
# TODO: Should be `int`
236+
reveal_type(b) # revealed: bool
237+
238+
if isinstance(a, bool) and is_int(a):
239+
reveal_type(a) # revealed: bool
240+
241+
if isinstance(a, bool) and guard_int(a):
242+
# TODO: Should be `int`
243+
reveal_type(a) # revealed: bool
244+
```

crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,33 @@ static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], Not[A
342342
static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy]))
343343
```
344344

345+
### `TypeGuard` and `TypeIs`
346+
347+
`TypeGuard[...]` and `TypeIs[...]` are subtypes of `bool`.
348+
349+
```py
350+
from ty_extensions import is_subtype_of, static_assert
351+
from typing_extensions import TypeGuard, TypeIs
352+
353+
# TODO: TypeGuard
354+
# static_assert(is_subtype_of(TypeGuard[int], bool))
355+
# static_assert(is_subtype_of(TypeIs[str], bool))
356+
```
357+
358+
`TypeIs` is invariant. `TypeGuard` is covariant.
359+
360+
```py
361+
from ty_extensions import is_subtype_of, static_assert
362+
from typing_extensions import TypeGuard, TypeIs
363+
364+
# TODO: TypeGuard
365+
# static_assert(is_subtype_of(TypeGuard[bool], TypeGuard[int]))
366+
367+
static_assert(not is_subtype_of(TypeGuard[int], TypeGuard[bool]))
368+
static_assert(not is_subtype_of(TypeIs[bool], TypeIs[int]))
369+
static_assert(not is_subtype_of(TypeIs[int], TypeIs[bool]))
370+
```
371+
345372
### Module literals
346373

347374
```py

0 commit comments

Comments
 (0)