[ty] Allow annotation expressions to be ast::Attribute nodes (#20413)

Fixes https://github.com/astral-sh/ty/issues/1187
This commit is contained in:
Alex Waygood 2025-09-15 12:06:48 +01:00 committed by GitHub
parent 1f1365a0fa
commit 8341da7f63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 65 additions and 44 deletions

View file

@ -9,6 +9,7 @@ For more details on the semantics of pure class variables, see [this test](../at
## Basic
```py
import typing
from typing import ClassVar, Annotated
class C:
@ -17,12 +18,14 @@ class C:
c: ClassVar[Annotated[int, "the annotation for c"]] = 1
d: ClassVar = 1
e: "ClassVar[int]" = 1
f: typing.ClassVar = 1
reveal_type(C.a) # revealed: int
reveal_type(C.b) # revealed: int
reveal_type(C.c) # revealed: int
reveal_type(C.d) # revealed: Unknown | Literal[1]
reveal_type(C.e) # revealed: int
reveal_type(C.f) # revealed: Unknown | Literal[1]
c = C()
@ -36,6 +39,8 @@ c.c = 2
c.d = 2
# error: [invalid-attribute-access]
c.e = 2
# error: [invalid-attribute-access]
c.f = 3
```
## From stubs

View file

@ -42,6 +42,53 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
&mut self,
annotation: &ast::Expr,
) -> TypeAndQualifiers<'db> {
fn infer_name_or_attribute<'db>(
ty: Type<'db>,
annotation: &ast::Expr,
builder: &TypeInferenceBuilder<'db, '_>,
) -> TypeAndQualifiers<'db> {
match ty {
Type::SpecialForm(SpecialFormType::ClassVar) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::CLASS_VAR)
}
Type::SpecialForm(SpecialFormType::Final) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL)
}
Type::SpecialForm(SpecialFormType::Required) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::REQUIRED)
}
Type::SpecialForm(SpecialFormType::NotRequired) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::NOT_REQUIRED)
}
Type::SpecialForm(SpecialFormType::ReadOnly) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::READ_ONLY)
}
Type::ClassLiteral(class) if class.is_known(builder.db(), KnownClass::InitVar) => {
if let Some(builder) =
builder.context.report_lint(&INVALID_TYPE_FORM, annotation)
{
builder
.into_diagnostic("`InitVar` may not be used without a type argument");
}
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::INIT_VAR)
}
_ => ty
.in_type_expression(
builder.db(),
builder.scope(),
builder.typevar_binding_context,
)
.unwrap_or_else(|error| {
error.into_fallback_type(
&builder.context,
annotation,
builder.is_reachable(annotation),
)
})
.into(),
}
}
// https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression
let annotation_ty = match annotation {
// String annotations: https://typing.python.org/en/latest/spec/annotations.html#string-annotations
@ -68,52 +115,21 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
TypeAndQualifiers::unknown()
}
ast::Expr::Attribute(attribute) => match attribute.ctx {
ast::ExprContext::Load => infer_name_or_attribute(
self.infer_attribute_expression(attribute),
annotation,
self,
),
ast::ExprContext::Invalid => TypeAndQualifiers::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {
todo_type!("Attribute expression annotation in Store/Del context").into()
}
},
ast::Expr::Name(name) => match name.ctx {
ast::ExprContext::Load => {
let name_expr_ty = self.infer_name_expression(name);
match name_expr_ty {
Type::SpecialForm(SpecialFormType::ClassVar) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::CLASS_VAR)
}
Type::SpecialForm(SpecialFormType::Final) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL)
}
Type::SpecialForm(SpecialFormType::Required) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::REQUIRED)
}
Type::SpecialForm(SpecialFormType::NotRequired) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::NOT_REQUIRED)
}
Type::SpecialForm(SpecialFormType::ReadOnly) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::READ_ONLY)
}
Type::ClassLiteral(class)
if class.is_known(self.db(), KnownClass::InitVar) =>
{
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_FORM, annotation)
{
builder.into_diagnostic(
"`InitVar` may not be used without a type argument",
);
}
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::INIT_VAR)
}
_ => name_expr_ty
.in_type_expression(
self.db(),
self.scope(),
self.typevar_binding_context,
)
.unwrap_or_else(|error| {
error.into_fallback_type(
&self.context,
annotation,
self.is_reachable(annotation),
)
})
.into(),
}
infer_name_or_attribute(self.infer_name_expression(name), annotation, self)
}
ast::ExprContext::Invalid => TypeAndQualifiers::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {