Skip to content

Commit beb8e2d

Browse files
sharkdpAlexWaygood
andauthored
[red-knot] More comprehensive is_assignable_to tests (#15353)
## Summary This changeset migrates all existing `is_assignable_to` tests to a Markdown-based test. It also increases our test coverage in a hopefully meaningful way (not claiming to be complete in any sense). But at least I found and fixed one bug while doing so. ## Test Plan Ran property tests to make sure the new test succeeds after fixing it. --------- Co-authored-by: Alex Waygood <[email protected]>
1 parent 88d0720 commit beb8e2d

File tree

3 files changed

+368
-81
lines changed

3 files changed

+368
-81
lines changed
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
# Assignable-to relation
2+
3+
The `is_assignable_to(S, T)` relation below checks if type `S` is assignable to type `T` (target).
4+
This allows us to check if a type `S` can be used in a context where a type `T` is expected
5+
(function arguments, variable assignments). See the [typing documentation] for a precise definition
6+
of this concept.
7+
8+
## Basic types
9+
10+
### Fully static
11+
12+
Fully static types participate in subtyping. If a type `S` is a subtype of `T`, `S` will also be
13+
assignable to `T`. Two equivalent types are subtypes of each other:
14+
15+
```py
16+
from knot_extensions import static_assert, is_assignable_to
17+
18+
class Parent: ...
19+
class Child1(Parent): ...
20+
class Child2(Parent): ...
21+
class Grandchild(Child1, Child2): ...
22+
class Unrelated: ...
23+
24+
static_assert(is_assignable_to(int, int))
25+
static_assert(is_assignable_to(Parent, Parent))
26+
static_assert(is_assignable_to(Child1, Parent))
27+
static_assert(is_assignable_to(Grandchild, Parent))
28+
static_assert(is_assignable_to(Unrelated, Unrelated))
29+
30+
static_assert(not is_assignable_to(str, int))
31+
static_assert(not is_assignable_to(object, int))
32+
static_assert(not is_assignable_to(Parent, Child1))
33+
static_assert(not is_assignable_to(Unrelated, Parent))
34+
static_assert(not is_assignable_to(Child1, Child2))
35+
```
36+
37+
### Gradual types
38+
39+
Gradual types do not participate in subtyping, but can still be assignable to other types (and
40+
static types can be assignable to gradual types):
41+
42+
```py
43+
from knot_extensions import static_assert, is_assignable_to, Unknown
44+
from typing import Any
45+
46+
static_assert(is_assignable_to(Unknown, Literal[1]))
47+
static_assert(is_assignable_to(Any, Literal[1]))
48+
static_assert(is_assignable_to(Literal[1], Unknown))
49+
static_assert(is_assignable_to(Literal[1], Any))
50+
```
51+
52+
## Literal types
53+
54+
### Boolean literals
55+
56+
`Literal[True]` and `Literal[False]` are both subtypes of (and therefore assignable to) `bool`,
57+
which is in turn a subtype of `int`:
58+
59+
```py
60+
from knot_extensions import static_assert, is_assignable_to
61+
from typing import Literal
62+
63+
static_assert(is_assignable_to(Literal[True], Literal[True]))
64+
static_assert(is_assignable_to(Literal[True], bool))
65+
static_assert(is_assignable_to(Literal[True], int))
66+
67+
static_assert(not is_assignable_to(Literal[True], Literal[False]))
68+
static_assert(not is_assignable_to(bool, Literal[True]))
69+
```
70+
71+
### Integer literals
72+
73+
```py
74+
from knot_extensions import static_assert, is_assignable_to
75+
from typing import Literal
76+
77+
static_assert(is_assignable_to(Literal[1], Literal[1]))
78+
static_assert(is_assignable_to(Literal[1], int))
79+
80+
static_assert(not is_assignable_to(Literal[1], Literal[2]))
81+
static_assert(not is_assignable_to(int, Literal[1]))
82+
static_assert(not is_assignable_to(Literal[1], str))
83+
```
84+
85+
### String literals and `LiteralString`
86+
87+
All string-literal types are subtypes of (and therefore assignable to) `LiteralString`, which is in
88+
turn a subtype of `str`:
89+
90+
```py
91+
from knot_extensions import static_assert, is_assignable_to
92+
from typing_extensions import Literal, LiteralString
93+
94+
static_assert(is_assignable_to(Literal["foo"], Literal["foo"]))
95+
static_assert(is_assignable_to(Literal["foo"], LiteralString))
96+
static_assert(is_assignable_to(Literal["foo"], str))
97+
98+
static_assert(is_assignable_to(LiteralString, str))
99+
100+
static_assert(not is_assignable_to(Literal["foo"], Literal["bar"]))
101+
static_assert(not is_assignable_to(str, Literal["foo"]))
102+
static_assert(not is_assignable_to(str, LiteralString))
103+
```
104+
105+
### Byte literals
106+
107+
```py
108+
from knot_extensions import static_assert, is_assignable_to
109+
from typing_extensions import Literal, LiteralString
110+
111+
static_assert(is_assignable_to(Literal[b"foo"], bytes))
112+
static_assert(is_assignable_to(Literal[b"foo"], Literal[b"foo"]))
113+
114+
static_assert(not is_assignable_to(Literal[b"foo"], str))
115+
static_assert(not is_assignable_to(Literal[b"foo"], LiteralString))
116+
static_assert(not is_assignable_to(Literal[b"foo"], Literal[b"bar"]))
117+
static_assert(not is_assignable_to(Literal[b"foo"], Literal["foo"]))
118+
static_assert(not is_assignable_to(Literal["foo"], Literal[b"foo"]))
119+
```
120+
121+
## `type[…]` and class literals
122+
123+
In the following tests, `TypeOf[str]` is a singleton type with a single inhabitant, the class `str`.
124+
This contrasts with `type[str]`, which represents "all possible subclasses of `str`".
125+
126+
Both `TypeOf[str]` and `type[str]` are subtypes of `type` and `type[object]`, which both represent
127+
"all possible instances of `type`"; therefore both `type[str]` and `TypeOf[str]` are assignable to
128+
`type`. `type[Any]`, on the other hand, represents a type of unknown size or inhabitants, but which
129+
is known to be no larger than the set of possible objects represented by `type`.
130+
131+
```py
132+
from knot_extensions import static_assert, is_assignable_to, Unknown, TypeOf
133+
from typing import Any
134+
135+
static_assert(is_assignable_to(type, type))
136+
static_assert(is_assignable_to(type[object], type[object]))
137+
138+
static_assert(is_assignable_to(type, type[object]))
139+
static_assert(is_assignable_to(type[object], type))
140+
141+
static_assert(is_assignable_to(type[str], type[object]))
142+
static_assert(is_assignable_to(TypeOf[str], type[object]))
143+
static_assert(is_assignable_to(type[str], type))
144+
static_assert(is_assignable_to(TypeOf[str], type))
145+
146+
static_assert(is_assignable_to(type[str], type[str]))
147+
static_assert(is_assignable_to(TypeOf[str], type[str]))
148+
149+
static_assert(not is_assignable_to(TypeOf[int], type[str]))
150+
static_assert(not is_assignable_to(type, type[str]))
151+
static_assert(not is_assignable_to(type[object], type[str]))
152+
153+
static_assert(is_assignable_to(type[Any], type[Any]))
154+
static_assert(is_assignable_to(type[Any], type[object]))
155+
static_assert(is_assignable_to(type[object], type[Any]))
156+
static_assert(is_assignable_to(type, type[Any]))
157+
static_assert(is_assignable_to(type[Any], type[str]))
158+
static_assert(is_assignable_to(type[str], type[Any]))
159+
static_assert(is_assignable_to(TypeOf[str], type[Any]))
160+
161+
static_assert(is_assignable_to(type[Unknown], type[Unknown]))
162+
static_assert(is_assignable_to(type[Unknown], type[object]))
163+
static_assert(is_assignable_to(type[object], type[Unknown]))
164+
static_assert(is_assignable_to(type, type[Unknown]))
165+
static_assert(is_assignable_to(type[Unknown], type[str]))
166+
static_assert(is_assignable_to(type[str], type[Unknown]))
167+
static_assert(is_assignable_to(TypeOf[str], type[Unknown]))
168+
169+
static_assert(is_assignable_to(type[Unknown], type[Any]))
170+
static_assert(is_assignable_to(type[Any], type[Unknown]))
171+
172+
static_assert(not is_assignable_to(object, type[Any]))
173+
static_assert(not is_assignable_to(str, type[Any]))
174+
175+
class Meta(type): ...
176+
177+
static_assert(is_assignable_to(type[Any], Meta))
178+
static_assert(is_assignable_to(type[Unknown], Meta))
179+
static_assert(is_assignable_to(Meta, type[Any]))
180+
static_assert(is_assignable_to(Meta, type[Unknown]))
181+
```
182+
183+
## Tuple types
184+
185+
```py
186+
from knot_extensions import static_assert, is_assignable_to
187+
from typing import Literal, Any
188+
189+
static_assert(is_assignable_to(tuple[()], tuple[()]))
190+
static_assert(is_assignable_to(tuple[int], tuple[int]))
191+
static_assert(is_assignable_to(tuple[int], tuple[Any]))
192+
static_assert(is_assignable_to(tuple[Any], tuple[int]))
193+
static_assert(is_assignable_to(tuple[int, str], tuple[int, str]))
194+
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, int]))
195+
static_assert(is_assignable_to(tuple[Any, Literal[2]], tuple[int, int]))
196+
static_assert(is_assignable_to(tuple[Literal[1], Any], tuple[int, int]))
197+
198+
static_assert(not is_assignable_to(tuple[()], tuple[int]))
199+
static_assert(not is_assignable_to(tuple[int], tuple[str]))
200+
static_assert(not is_assignable_to(tuple[int], tuple[int, str]))
201+
static_assert(not is_assignable_to(tuple[int, str], tuple[int]))
202+
static_assert(not is_assignable_to(tuple[int, int], tuple[Literal[1], int]))
203+
static_assert(not is_assignable_to(tuple[Any, Literal[2]], tuple[int, str]))
204+
```
205+
206+
## Union types
207+
208+
```py
209+
from knot_extensions import static_assert, is_assignable_to, Unknown
210+
from typing import Literal, Any
211+
212+
static_assert(is_assignable_to(int, int | str))
213+
static_assert(is_assignable_to(str, int | str))
214+
static_assert(is_assignable_to(int | str, int | str))
215+
static_assert(is_assignable_to(str | int, int | str))
216+
static_assert(is_assignable_to(Literal[1], int | str))
217+
static_assert(is_assignable_to(Literal[1], Unknown | str))
218+
static_assert(is_assignable_to(Literal[1] | Literal[2], Literal[1] | Literal[2]))
219+
static_assert(is_assignable_to(Literal[1] | Literal[2], int))
220+
static_assert(is_assignable_to(Literal[1] | None, int | None))
221+
static_assert(is_assignable_to(Any, int | str))
222+
static_assert(is_assignable_to(Any | int, int))
223+
static_assert(is_assignable_to(str, int | Any))
224+
225+
static_assert(not is_assignable_to(int | None, int))
226+
static_assert(not is_assignable_to(int | None, str | None))
227+
static_assert(not is_assignable_to(Literal[1] | None, int))
228+
static_assert(not is_assignable_to(Literal[1] | None, str | None))
229+
static_assert(not is_assignable_to(Any | int | str, int))
230+
```
231+
232+
## Intersection types
233+
234+
```py
235+
from knot_extensions import static_assert, is_assignable_to, Intersection, Not
236+
from typing_extensions import Any, Literal
237+
238+
class Parent: ...
239+
class Child1(Parent): ...
240+
class Child2(Parent): ...
241+
class Grandchild(Child1, Child2): ...
242+
class Unrelated: ...
243+
244+
static_assert(is_assignable_to(Intersection[Child1, Child2], Child1))
245+
static_assert(is_assignable_to(Intersection[Child1, Child2], Child2))
246+
static_assert(is_assignable_to(Intersection[Child1, Child2], Parent))
247+
static_assert(is_assignable_to(Intersection[Child1, Parent], Parent))
248+
249+
static_assert(is_assignable_to(Intersection[Parent, Unrelated], Parent))
250+
static_assert(is_assignable_to(Intersection[Child1, Unrelated], Child1))
251+
252+
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Child1))
253+
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Parent))
254+
static_assert(is_assignable_to(Intersection[Child1, Not[Grandchild]], Parent))
255+
256+
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child1, Child2]))
257+
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child2, Child1]))
258+
static_assert(is_assignable_to(Grandchild, Intersection[Child1, Child2]))
259+
260+
static_assert(not is_assignable_to(Parent, Intersection[Parent, Unrelated]))
261+
static_assert(not is_assignable_to(int, Intersection[int, Not[Literal[1]]]))
262+
static_assert(not is_assignable_to(int, Not[int]))
263+
static_assert(not is_assignable_to(int, Not[Literal[1]]))
264+
265+
static_assert(not is_assignable_to(Intersection[Any, Parent], Unrelated))
266+
267+
# TODO: The following assertions should not fail (see https://github.com/astral-sh/ruff/issues/14899)
268+
# error: [static-assert-error]
269+
static_assert(is_assignable_to(Intersection[Any, int], int))
270+
271+
# error: [static-assert-error]
272+
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any]))
273+
# error: [static-assert-error]
274+
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))
275+
# error: [static-assert-error]
276+
static_assert(is_assignable_to(Intersection[Unrelated, Any], Not[tuple[Unrelated, Any]]))
277+
```
278+
279+
## General properties
280+
281+
See also: our property tests in `property_tests.rs`.
282+
283+
### Everything is assignable to `object`
284+
285+
`object` is Python's top type; the set of all possible objects at runtime:
286+
287+
```py
288+
from knot_extensions import static_assert, is_assignable_to, Unknown
289+
from typing import Literal, Any
290+
291+
static_assert(is_assignable_to(str, object))
292+
static_assert(is_assignable_to(Literal[1], object))
293+
static_assert(is_assignable_to(object, object))
294+
static_assert(is_assignable_to(type, object))
295+
static_assert(is_assignable_to(Any, object))
296+
static_assert(is_assignable_to(Unknown, object))
297+
static_assert(is_assignable_to(type[object], object))
298+
static_assert(is_assignable_to(type[str], object))
299+
static_assert(is_assignable_to(type[Any], object))
300+
```
301+
302+
### Every type is assignable to `Any` / `Unknown`
303+
304+
`Any` and `Unknown` are gradual types. They could materialize to any given type at runtime, and so
305+
any type is assignable to them:
306+
307+
```py
308+
from knot_extensions import static_assert, is_assignable_to, Unknown
309+
from typing import Literal, Any
310+
311+
static_assert(is_assignable_to(str, Any))
312+
static_assert(is_assignable_to(Literal[1], Any))
313+
static_assert(is_assignable_to(object, Any))
314+
static_assert(is_assignable_to(type, Any))
315+
static_assert(is_assignable_to(Any, Any))
316+
static_assert(is_assignable_to(Unknown, Any))
317+
static_assert(is_assignable_to(type[object], Any))
318+
static_assert(is_assignable_to(type[str], Any))
319+
static_assert(is_assignable_to(type[Any], Any))
320+
321+
static_assert(is_assignable_to(str, Unknown))
322+
static_assert(is_assignable_to(Literal[1], Unknown))
323+
static_assert(is_assignable_to(object, Unknown))
324+
static_assert(is_assignable_to(type, Unknown))
325+
static_assert(is_assignable_to(Any, Unknown))
326+
static_assert(is_assignable_to(Unknown, Unknown))
327+
static_assert(is_assignable_to(type[object], Unknown))
328+
static_assert(is_assignable_to(type[str], Unknown))
329+
static_assert(is_assignable_to(type[Any], Unknown))
330+
```
331+
332+
### `Never` is assignable to every type
333+
334+
`Never` is Python's bottom type: the empty set, a type with no inhabitants. It is therefore
335+
assignable to any arbitrary type.
336+
337+
```py
338+
from knot_extensions import static_assert, is_assignable_to, Unknown
339+
from typing_extensions import Never, Any
340+
341+
static_assert(is_assignable_to(Never, str))
342+
static_assert(is_assignable_to(Never, Literal[1]))
343+
static_assert(is_assignable_to(Never, object))
344+
static_assert(is_assignable_to(Never, type))
345+
static_assert(is_assignable_to(Never, Any))
346+
static_assert(is_assignable_to(Never, Unknown))
347+
static_assert(is_assignable_to(Never, type[object]))
348+
static_assert(is_assignable_to(Never, type[str]))
349+
static_assert(is_assignable_to(Never, type[Any]))
350+
```
351+
352+
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation

0 commit comments

Comments
 (0)