Skip to content

Commit 90b32f3

Browse files
authored
[ty] Ensure annotation/type expressions in stub files are always deferred (#21401)
1 parent 99694b6 commit 90b32f3

File tree

8 files changed

+115
-5
lines changed

8 files changed

+115
-5
lines changed

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,34 @@ B = NewType("B", list[Any])
396396
T = TypeVar("T")
397397
C = NewType("C", list[T]) # TODO: should be "error: [invalid-newtype]"
398398
```
399+
400+
## Forward references in stub files
401+
402+
Stubs natively support forward references, so patterns that would raise `NameError` at runtime are
403+
allowed in stub files:
404+
405+
`stub.pyi`:
406+
407+
```pyi
408+
from typing import NewType
409+
410+
N = NewType("N", A)
411+
412+
class A: ...
413+
```
414+
415+
`main.py`:
416+
417+
```py
418+
from stub import N, A
419+
420+
n = N(A()) # fine
421+
422+
def f(x: A): ...
423+
424+
f(n) # fine
425+
426+
class Invalid: ...
427+
428+
bad = N(Invalid()) # error: [invalid-argument-type]
429+
```

crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,48 @@ from typing import TypeVar
266266

267267
# error: [invalid-legacy-type-variable]
268268
T = TypeVar("T", invalid_keyword=True)
269+
```
270+
271+
### Forward references in stubs
272+
273+
Stubs natively support forward references, so patterns that would raise `NameError` at runtime are
274+
allowed in stub files:
275+
276+
`stub.pyi`:
277+
278+
```pyi
279+
from typing import TypeVar
280+
281+
T = TypeVar("T", bound=A, default=B)
282+
U = TypeVar("U", C, D)
283+
284+
class A: ...
285+
class B(A): ...
286+
class C: ...
287+
class D: ...
288+
289+
def f(x: T) -> T: ...
290+
def g(x: U) -> U: ...
291+
```
292+
293+
`main.py`:
294+
295+
```py
296+
from stub import f, g, A, B, C, D
297+
298+
reveal_type(f(A())) # revealed: A
299+
reveal_type(f(B())) # revealed: B
300+
reveal_type(g(C())) # revealed: C
301+
reveal_type(g(D())) # revealed: D
302+
303+
# TODO: one diagnostic would probably be sufficient here...?
304+
#
305+
# error: [invalid-argument-type] "Argument type `C` does not satisfy upper bound `A` of type variable `T`"
306+
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `B`, found `C`"
307+
reveal_type(f(C())) # revealed: B
269308

309+
# error: [invalid-argument-type]
310+
reveal_type(g(A())) # revealed: Unknown
270311
```
271312

272313
### Constructor signature versioning

crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def _(x: int | str | bytes | memoryview | range):
8282
if isinstance(x, int | str):
8383
reveal_type(x) # revealed: int | str
8484
elif isinstance(x, bytes | memoryview):
85-
reveal_type(x) # revealed: bytes | memoryview[Unknown]
85+
reveal_type(x) # revealed: bytes | memoryview[int]
8686
else:
8787
reveal_type(x) # revealed: range
8888
```
@@ -242,11 +242,11 @@ def _(flag: bool):
242242
def _(flag: bool):
243243
x = 1 if flag else "a"
244244

245-
# error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `Literal["a"]"
245+
# error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Divergent, ...]`, found `Literal["a"]"
246246
if isinstance(x, "a"):
247247
reveal_type(x) # revealed: Literal[1, "a"]
248248

249-
# error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `Literal["int"]"
249+
# error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Divergent, ...]`, found `Literal["int"]"
250250
if isinstance(x, "int"):
251251
reveal_type(x) # revealed: Literal[1, "a"]
252252
```

crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def flag() -> bool:
283283

284284
t = int if flag() else str
285285

286-
# error: [invalid-argument-type] "Argument to function `issubclass` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `Literal["str"]"
286+
# error: [invalid-argument-type] "Argument to function `issubclass` is incorrect: Expected `type | UnionType | tuple[Divergent, ...]`, found `Literal["str"]"
287287
if issubclass(t, "str"):
288288
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
289289

crates/ty_python_semantic/resources/mdtest/paramspec.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,20 @@ Other values are invalid.
102102
P4 = ParamSpec("P4", default=int)
103103
```
104104

105+
### Forward references in stub files
106+
107+
Stubs natively support forward references, so patterns that would raise `NameError` at runtime are
108+
allowed in stub files:
109+
110+
```pyi
111+
from typing_extensions import ParamSpec
112+
113+
P = ParamSpec("P", default=[A, B])
114+
115+
class A: ...
116+
class B: ...
117+
```
118+
105119
### PEP 695
106120

107121
```toml

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6921,6 +6921,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
69216921
ty
69226922
}
69236923

6924+
#[track_caller]
69246925
fn store_expression_type(&mut self, expression: &ast::Expr, ty: Type<'db>) {
69256926
if self.deferred_state.in_string_annotation() {
69266927
// Avoid storing the type of expressions that are part of a string annotation because

crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
1818
annotation: &ast::Expr,
1919
deferred_state: DeferredExpressionState,
2020
) -> TypeAndQualifiers<'db> {
21-
let previous_deferred_state = std::mem::replace(&mut self.deferred_state, deferred_state);
21+
// `DeferredExpressionState::InStringAnnotation` takes precedence over other deferred states.
22+
// However, if it's not a stringified annotation, we must still ensure that annotation expressions
23+
// are always deferred in stub files.
24+
let state = if deferred_state.in_string_annotation() {
25+
deferred_state
26+
} else if self.in_stub() {
27+
DeferredExpressionState::Deferred
28+
} else {
29+
deferred_state
30+
};
31+
32+
let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state);
2233
let annotation_ty = self.infer_annotation_expression_impl(annotation);
2334
self.deferred_state = previous_deferred_state;
2435
annotation_ty

crates/ty_python_semantic/src/types/infer/builder/type_expression.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ use crate::types::{
2020
impl<'db> TypeInferenceBuilder<'db, '_> {
2121
/// Infer the type of a type expression.
2222
pub(super) fn infer_type_expression(&mut self, expression: &ast::Expr) -> Type<'db> {
23+
// `DeferredExpressionState::InStringAnnotation` takes precedence over other states.
24+
// However, if it's not a stringified annotation, we must still ensure that annotation expressions
25+
// are always deferred in stub files.
26+
match self.deferred_state {
27+
DeferredExpressionState::None => {
28+
if self.in_stub() {
29+
self.deferred_state = DeferredExpressionState::Deferred;
30+
}
31+
}
32+
DeferredExpressionState::InStringAnnotation(_) | DeferredExpressionState::Deferred => {}
33+
}
34+
2335
let mut ty = self.infer_type_expression_no_store(expression);
2436
let divergent = Type::divergent(Some(self.scope()));
2537
if ty.has_divergent_type(self.db(), divergent) {

0 commit comments

Comments
 (0)