Skip to content

Commit e0d2cfb

Browse files
committed
Test suite improvements
1 parent 403108d commit e0d2cfb

File tree

1 file changed

+87
-49
lines changed

1 file changed

+87
-49
lines changed

crates/ty_python_semantic/resources/mdtest/public_types.md

Lines changed: 87 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
## Basic
44

5-
The "public type" of a symbol refers to the type that is inferred for a symbol from another scope.
6-
Since it is not generally possible to analyze the full control flow of a program, we currently make
7-
the assumption that the inner scope (such as the inner function below) could be executed at any
8-
position. The public type should therefore be the union of all possible types that the symbol could
9-
have.
5+
The "public type" of a symbol refers to the type that is inferred for a symbol from an enclosing
6+
scope. Since it is not generally possible to analyze the full control flow of a program, we
7+
currently make the simplifying assumption that the inner scope (such as the `inner` function below)
8+
could be executed at any position in the enclosing scope. The public type should therefore be the
9+
union of all possible types that the symbol could have.
1010

1111
In the following example, depending on when `inner()` is called, the type of `x` could either be `A`
1212
or `B`:
@@ -21,10 +21,12 @@ def outer() -> None:
2121

2222
def inner() -> None:
2323
reveal_type(x) # revealed: Unknown | A | B
24+
# This call would observe `x` as `A`.
2425
inner()
2526

2627
x = B()
2728

29+
# This call would observe `x` as `B`.
2830
inner()
2931
```
3032

@@ -61,7 +63,7 @@ def outer() -> None:
6163
inner()
6264

6365
if False:
64-
x = B()
66+
x = B() # this binding of `x` is unreachable
6567
inner()
6668

6769
x = C()
@@ -77,7 +79,7 @@ def outer(flag: bool) -> None:
7779
if flag:
7880
return
7981

80-
x = B()
82+
x = B() # this binding of `x` is unreachable
8183

8284
x = C()
8385
inner()
@@ -108,7 +110,9 @@ def outer(flag: bool) -> None:
108110
inner()
109111
```
110112

111-
The public type is available even if the end of the outer scope is unreachable:
113+
The public type is available, even if the end of the outer scope is unreachable. This is a
114+
regression test. A previous version of ty used the end-of-scope position to determine the public
115+
type, which would have resulted in wrong types here:
112116

113117
```py
114118
def outer() -> None:
@@ -133,52 +137,38 @@ def outer(flag: bool) -> None:
133137
# unreachable
134138

135139
inner()
136-
```
137-
138-
This works at arbitrary levels of nesting:
139-
140-
```py
141-
def outer() -> None:
142-
x = A()
143-
144-
def intermediate() -> None:
145-
def inner() -> None:
146-
reveal_type(x) # revealed: Unknown | A | B
147-
inner()
148-
intermediate()
149-
150-
x = B()
151-
152-
intermediate()
153140

154141
def outer(x: A) -> None:
155142
def inner() -> None:
156143
reveal_type(x) # revealed: A
157144
raise
158145
```
159146

160-
## Interplay with type narrowing
147+
Arbitrary many levels of nesting are supported:
161148

162149
```py
163-
class A: ...
150+
def f0() -> None:
151+
x = A()
164152

165-
def outer(x: A | None):
166-
def inner() -> None:
167-
reveal_type(x) # revealed: A | None
168-
inner()
169-
if x is None:
170-
inner()
153+
def f1() -> None:
154+
def f2() -> None:
155+
def f3() -> None:
156+
def f4() -> None:
157+
reveal_type(x) # revealed: Unknown | A | B
158+
f4()
159+
f3()
160+
f2()
161+
f1()
171162

172-
def outer(x: A | None):
173-
if x is not None:
174-
def inner() -> None:
175-
# TODO: should ideally be `A`
176-
reveal_type(x) # revealed: A | None
177-
inner()
163+
x = B()
164+
165+
f1()
178166
```
179167

180168
## At module level
181169

170+
The behavior is the same if the outer scope is the global scope of a module:
171+
182172
```py
183173
def flag() -> bool:
184174
return True
@@ -199,51 +189,97 @@ if flag():
199189

200190
## Limitations
201191

192+
### Type narrowing
193+
194+
We currently do not further analyze control flow, so we do not support cases where the inner scope
195+
is only executed in a branch where the type of `x` is narrowed:
196+
197+
```py
198+
class A: ...
199+
200+
def outer(x: A | None):
201+
if x is not None:
202+
def inner() -> None:
203+
# TODO: should ideally be `A`
204+
reveal_type(x) # revealed: A | None
205+
inner()
206+
```
207+
208+
### Shadowing
209+
210+
Similarly, since we do not analyze control flow in the outer scope here, we assume that `inner()`
211+
could be called between the two assignments to `x`:
212+
213+
```py
214+
def outer() -> None:
215+
def inner() -> None:
216+
# TODO: this should ideally be `Unknown | Literal[1]`, but no other type checker supports this either
217+
reveal_type(x) # revealed: Unknown | None | Literal[1]
218+
x = None
219+
220+
# [additional code here]
221+
222+
x = 1
223+
224+
inner()
225+
```
226+
227+
This is currently even true if the `inner` function is only defined after the second assignment to
228+
`x`:
229+
202230
```py
203-
def outer():
231+
def outer() -> None:
204232
x = None
205233

206-
# []
234+
# [additional code here]
207235

208236
x = 1
209237

210-
def inner():
211-
# TODO: this should ideally be `Unknown | Literal[1]`
238+
def inner() -> None:
239+
# TODO: this should be `Unknown | Literal[1]`. Mypy and pyright support this.
212240
reveal_type(x) # revealed: Unknown | None | Literal[1]
213241
inner()
214242
```
215243

216-
Similar:
244+
A similar case derived from an ecosystem example, involving declared types:
217245

218246
```py
219247
class C: ...
220248

221-
def _f_(x: C | None):
249+
def outer(x: C | None):
222250
x = x or C()
223251

224252
reveal_type(x) # revealed: C
225253

226-
def g():
254+
def inner() -> None:
227255
# TODO: this should ideally be `C`
228256
reveal_type(x) # revealed: C | None
257+
inner()
229258
```
230259

231-
Writes to the outer-scope variable are not detected. Other typecheckers also don't support this:
260+
### Assignments to nonlocal variables
261+
262+
Writes to the outer-scope variable are currently not detected:
232263

233264
```py
234-
def outer():
265+
def outer() -> None:
235266
x = None
236267

237268
def set_x() -> None:
269+
nonlocal x
238270
x = 1
239271
set_x()
240272

241273
def inner() -> None:
274+
# TODO: this should ideally be `Unknown | None | Literal[1]`.
242275
reveal_type(x) # revealed: Unknown | None
243276
inner()
244277
```
245278

246-
## Overloads
279+
## Handling of overloads
280+
281+
Overloads need special treatment, because here, we do not want to consider *all* possible
282+
definitions of `f`. This would otherwise result in a union of all three definitions of `f`:
247283

248284
```py
249285
from typing import overload
@@ -255,10 +291,12 @@ def f(x: str) -> str: ...
255291
def f(x: int | str) -> int | str:
256292
raise NotImplementedError
257293

294+
reveal_type(f) # revealed: Overload[(x: int) -> int, (x: str) -> str]
258295
reveal_type(f(1)) # revealed: int
259296
reveal_type(f("a")) # revealed: str
260297

261298
def _():
299+
reveal_type(f) # revealed: Overload[(x: int) -> int, (x: str) -> str]
262300
reveal_type(f(1)) # revealed: int
263301
reveal_type(f("a")) # revealed: str
264302
```

0 commit comments

Comments
 (0)