Skip to content

Commit f518cc9

Browse files
authored
[ty] Allow partially stringified type[…] annotations (#24518)
## Summary Allow partially stringified `type["ForwardRef"]` annotations, even if they are not explicitly allowed by the typing spec. The implementation here follows what we do in e.g. `infer_string_type_expression`. closes astral-sh/ty#3244 ## Ecosystem We apparently get rid of a prevalent `@Todo` type, so we see new diagnostics, but nothing that looks like *new* false positives, related to this change. A lot of ecosystem hits have an ignore-comment for another type checker, which is generally a good signal. ## Test Plan * New Markdown tests * Verified that it fixes the problem in astral-sh/ty#3244
1 parent 16c4090 commit f518cc9

3 files changed

Lines changed: 92 additions & 12 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ python-version = "3.9"
227227
```
228228

229229
```py
230+
# error: [unsupported-operator]
230231
def _(x: type[int | str | bytes]):
231232
# error: [unsupported-operator]
232233
if issubclass(x, int | str):

crates/ty_python_semantic/resources/mdtest/type_of/basic.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,91 @@ f(types.NoneType)
155155
f(None) # error: [invalid-argument-type]
156156
```
157157

158+
## Stringified annotations
159+
160+
### Basic
161+
162+
Stringified and partially stringified `type[…]` annotations are supported, even if the latter are
163+
not explicitly allowed by the [syntax in the typing spec].
164+
165+
```py
166+
def _(
167+
type_of_foo_1: "type[Foo]",
168+
type_of_foo_2: type["Foo"],
169+
type_of_foo_or_bar_1: "type[Foo | Bar]",
170+
type_of_foo_or_bar_2: type["Foo | Bar"],
171+
):
172+
reveal_type(type_of_foo_1) # revealed: type[Foo]
173+
reveal_type(type_of_foo_2) # revealed: type[Foo]
174+
reveal_type(type_of_foo_or_bar_1) # revealed: type[Foo | Bar]
175+
reveal_type(type_of_foo_or_bar_2) # revealed: type[Foo | Bar]
176+
177+
class Foo: ...
178+
class Bar: ...
179+
```
180+
181+
Illegal stringified annotations lead to a diagnostic:
182+
183+
```py
184+
# error: [invalid-syntax-in-forward-annotation]
185+
def _(type_of_invalid: type[""]):
186+
reveal_type(type_of_invalid) # revealed: type[Unknown]
187+
```
188+
189+
### Unions of strings, Python 3.13
190+
191+
"Unions of strings" lead to a runtime error on 3.13 and lower, so we emit a diagnostic. We still
192+
infer `type[Foo | Bar]` though, since the intention seems clear.
193+
194+
```toml
195+
[environment]
196+
python-version = "3.13"
197+
```
198+
199+
```py
200+
def _(type_of_invalid: type["Foo" | "Bar"]): # error: [unsupported-operator]
201+
reveal_type(type_of_invalid) # revealed: type[Foo | Bar]
202+
203+
class Foo: ...
204+
class Bar: ...
205+
```
206+
207+
### Unions of strings, Python 3.13 with `from __future__ import annotations`
208+
209+
On Python 3.13 with `from __future__ import annotations`, there is no error:
210+
211+
```toml
212+
[environment]
213+
python-version = "3.13"
214+
```
215+
216+
```py
217+
from __future__ import annotations
218+
219+
def _(type_of_foo_or_bar: type["Foo" | "Bar"]):
220+
reveal_type(type_of_foo_or_bar) # revealed: type[Foo | Bar]
221+
222+
class Foo: ...
223+
class Bar: ...
224+
```
225+
226+
### Unions of strings, Python 3.14
227+
228+
On Python 3.14 and higher, this is also fine:
229+
230+
```toml
231+
[environment]
232+
python-version = "3.14"
233+
```
234+
235+
```py
236+
def _(type_of_foo_or_bar: type["Foo" | "Bar"]):
237+
reveal_type(type_of_foo_or_bar) # revealed: type[Foo | Bar]
238+
239+
class Foo: ...
240+
class Bar: ...
241+
```
242+
158243
## Illegal parameters
159244

160245
```py
@@ -496,3 +581,5 @@ def f(
496581
reveal_type(c) # revealed: <class 'Bar'> | (() -> Bar)
497582
reveal_type(d) # revealed: CustomCallback
498583
```
584+
585+
[syntax in the typing spec]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions

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

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,18 +1091,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
10911091
};
10921092

10931093
match slice {
1094-
ast::Expr::Name(_) | ast::Expr::Attribute(_) => infer_type_argument(self, slice),
1094+
ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::StringLiteral(_) => {
1095+
infer_type_argument(self, slice)
1096+
}
10951097
ast::Expr::BinOp(binary) if binary.op == ast::Operator::BitOr => {
1096-
let union_ty = UnionType::from_elements_leave_aliases(
1097-
self.db(),
1098-
[
1099-
self.infer_subclass_of_type_expression(&binary.left),
1100-
self.infer_subclass_of_type_expression(&binary.right),
1101-
],
1102-
);
1103-
self.store_expression_type(slice, union_ty);
1104-
1105-
union_ty
1098+
infer_type_argument(self, slice)
11061099
}
11071100
ast::Expr::Tuple(_) => {
11081101
if !self.in_string_annotation() {
@@ -1193,7 +1186,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
11931186
self.store_expression_type(slice, parameters_ty);
11941187
parameters_ty
11951188
}
1196-
// TODO: subscripts, etc.
11971189
_ => {
11981190
self.infer_expression(slice, TypeContext::default());
11991191
todo_type!("unsupported type[X] special form")

0 commit comments

Comments
 (0)