diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index 87a25e90e1..398b897120 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -2694,9 +2694,11 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { Type::Annotated(_, _) => acc.push(AttributeBase1::ClassInstance( self.stdlib.generic_alias().clone(), )), + Type::TypeForm(_) => { + acc.push(AttributeBase1::ClassInstance(self.stdlib.object().clone())) + } // TODO: check to see which ones should have class representations Type::SpecialForm(_) - | Type::TypeForm(_) | Type::TypeLevelDslCall(_) | Type::Unpack(_) | Type::Concatenate(_, _) diff --git a/pyrefly/lib/test/typeform.rs b/pyrefly/lib/test/typeform.rs index 03a6c12ed4..8b9f25e766 100644 --- a/pyrefly/lib/test/typeform.rs +++ b/pyrefly/lib/test/typeform.rs @@ -221,3 +221,29 @@ X: TypeForm[int] = int x: X = 0 # E: Expected a type form, got instance of `TypeForm[int]` "#, ); + +testcase!( + test_typeform_attribute_access, + r#" +import typing +from typing import TypeVar +from typing_extensions import TypeForm + +T = TypeVar("T") + +def field_names(tp: TypeForm[T]): + if typing.is_typeddict(tp): + return tp.__annotations__.keys() + return () + "#, +); + +testcase!( + test_typeform_attribute_access_unknown, + r#" +from typing_extensions import TypeForm + +def f(tp: TypeForm[int]): + tp.not_a_real_attribute # E: Object of class `object` has no attribute `not_a_real_attribute` + "#, +);