[ty] Support stringified annotations in value-position Annotated instances (#21447)

## Summary

Infer the first argument `type` inside `Annotated[type, …]` as a type
expression. This allows us to support stringified annotations inside
`Annotated`.

## Ecosystem

* The removed diagnostic on `prefect` shows that we now understand the
`State.data` type annotation in
`src/prefect/client/schemas/objects.py:230`, which uses a stringified
annotation in `Annoated`. The other diagnostics are downstream changes
that result from this, it seems to be a commonly used data type.
* `artigraph` does something like `Annotated[cast(Any,
field_info.annotation), *field_info.metadata]` which I'm not sure we
need to allow? It's unfortunate since this is probably supported at
runtime, but it seems reasonable that they need to add a `# type:
ignore` for that.
* `pydantic` uses something like `Annotated[(self.annotation,
*self.metadata)]` but adds a `# type: ignore`

## Test Plan

New Markdown test
This commit is contained in:
David Peter 2025-11-14 14:09:09 +01:00 committed by GitHub
parent 05cf53aae8
commit e9a5337136
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 28 additions and 9 deletions

View file

@ -76,7 +76,18 @@ from ty_extensions import reveal_mro
class C(Annotated[int, "foo"]): ...
reveal_mro(C) # revealed: (<class 'C'>, <class 'int'>, <class 'object'>)
# revealed: (<class 'C'>, <class 'int'>, <class 'object'>)
reveal_mro(C)
class D(Annotated[list[str], "foo"]): ...
# revealed: (<class 'D'>, <class 'list[str]'>, <class 'MutableSequence[str]'>, <class 'Sequence[str]'>, <class 'Reversible[str]'>, <class 'Collection[str]'>, <class 'Iterable[str]'>, <class 'Container[str]'>, typing.Protocol, typing.Generic, <class 'object'>)
reveal_mro(D)
class E(Annotated[list["E"], "metadata"]): ...
# error: [revealed-type] "Revealed MRO: (<class 'E'>, <class 'list[E]'>, <class 'MutableSequence[E]'>, <class 'Sequence[E]'>, <class 'Reversible[E]'>, <class 'Collection[E]'>, <class 'Iterable[E]'>, <class 'Container[E]'>, typing.Protocol, typing.Generic, <class 'object'>)"
reveal_mro(E)
```
### Not parameterized

View file

@ -974,13 +974,14 @@ We *do* support stringified annotations if they appear in a position where a typ
syntactically expected:
```py
from typing import Union, List, Dict
from typing import Union, List, Dict, Annotated
ListOfInts1 = list["int"]
ListOfInts2 = List["int"]
StrOrStyle = Union[str, "Style"]
SubclassOfStyle = type["Style"]
DictStrToStyle = Dict[str, "Style"]
AnnotatedStyle = Annotated["Style", "metadata"]
class Style: ...
@ -990,12 +991,14 @@ def _(
str_or_style: StrOrStyle,
subclass_of_style: SubclassOfStyle,
dict_str_to_style: DictStrToStyle,
annotated_style: AnnotatedStyle,
):
reveal_type(list_of_ints1) # revealed: list[int]
reveal_type(list_of_ints2) # revealed: list[int]
reveal_type(str_or_style) # revealed: str | Style
reveal_type(subclass_of_style) # revealed: type[Style]
reveal_type(dict_str_to_style) # revealed: dict[str, Style]
reveal_type(annotated_style) # revealed: Style
```
## Recursive

View file

@ -6599,11 +6599,7 @@ impl<'db> Type<'db> {
Ok(builder.build())
}
KnownInstanceType::Literal(ty) => Ok(ty.inner(db)),
KnownInstanceType::Annotated(ty) => {
Ok(ty
.inner(db)
.in_type_expression(db, scope_id, typevar_binding_context)?)
}
KnownInstanceType::Annotated(ty) => Ok(ty.inner(db)),
KnownInstanceType::TypeGenericAlias(ty) => {
// When `type[…]` appears in a value position (e.g. in an implicit type alias),
// we infer its argument as a type expression. This ensures that we can emit

View file

@ -183,7 +183,16 @@ impl<'db> ClassBase<'db> {
KnownInstanceType::TypeGenericAlias(_) => {
Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass)
}
KnownInstanceType::Annotated(ty) => Self::try_from_type(db, ty.inner(db), subclass),
KnownInstanceType::Annotated(ty) => {
// Unions are not supported in this position, so we only need to support
// something like `class C(Annotated[Base, "metadata"]): ...`, which we
// can do by turning the instance type (`Base` in this example) back into
// a class.
let annotated_ty = ty.inner(db);
let instance_ty = annotated_ty.as_nominal_instance()?;
Some(Self::Class(instance_ty.class(db)))
}
},
Type::SpecialForm(special_form) => match special_form {

View file

@ -10726,7 +10726,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_expression(element, TypeContext::default());
}
let ty = self.infer_expression(type_expr, TypeContext::default());
let ty = self.infer_type_expression(type_expr);
return Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new(
self.db(),