diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses.md index 54ac25998b3880..7b5377d0132a38 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses.md @@ -616,6 +616,25 @@ reveal_type(C.__init__) # revealed: (field: str | int = int) -> None To do +## `dataclass.fields` + +Dataclasses have `__dataclass_fields__` in them, which makes them a subtype of the +`DataclassInstance` protocol. + +Here, we verify that dataclasses can be passed to `dataclasses.fields` without any errors, and that +the return type of `dataclasses.fields` is correct. + +```py +from dataclasses import dataclass, fields + +@dataclass +class Foo: + x: int + +reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, Field[Any]] +reveal_type(fields(Foo)) # revealed: tuple[Field[Any], ...] +``` + ## Other special cases ### `dataclasses.dataclass` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 9e536cbf31f0b8..3eb740871c27e1 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2939,6 +2939,19 @@ impl<'db> Type<'db> { )) .into() } + Type::ClassLiteral(class) + if name == "__dataclass_fields__" && class.dataclass_params(db).is_some() => + { + // Make this class look like a subclass of the `DataClassInstance` protocol + Symbol::bound(KnownClass::Dict.to_specialized_instance( + db, + [ + KnownClass::Str.to_instance(db), + KnownClass::Field.to_specialized_instance(db, [Type::any()]), + ], + )) + .with_qualifiers(TypeQualifiers::CLASS_VAR) + } Type::BoundMethod(bound_method) => match name_str { "__self__" => Symbol::bound(bound_method.self_instance(db)).into(), "__func__" => { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 96673012f1f498..0f7936a18d5587 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1958,6 +1958,8 @@ pub enum KnownClass { // backported as `builtins.ellipsis` by typeshed on Python <=3.9 EllipsisType, NotImplementedType, + // dataclasses + Field, } impl<'db> KnownClass { @@ -2037,7 +2039,8 @@ impl<'db> KnownClass { // and raises a `TypeError` in Python >=3.14 // (see https://docs.python.org/3/library/constants.html#NotImplemented) | Self::NotImplementedType - | Self::Classmethod => Truthiness::Ambiguous, + | Self::Classmethod + | Self::Field => Truthiness::Ambiguous, } } @@ -2108,7 +2111,8 @@ impl<'db> KnownClass { | Self::VersionInfo | Self::EllipsisType | Self::NotImplementedType - | Self::UnionType => false, + | Self::UnionType + | Self::Field => false, } } @@ -2181,6 +2185,7 @@ impl<'db> KnownClass { } } Self::NotImplementedType => "_NotImplementedType", + Self::Field => "Field", } } @@ -2405,6 +2410,7 @@ impl<'db> KnownClass { | Self::DefaultDict | Self::Deque | Self::OrderedDict => KnownModule::Collections, + Self::Field => KnownModule::Dataclasses, } } @@ -2464,7 +2470,8 @@ impl<'db> KnownClass { | Self::ABCMeta | Self::Super | Self::NamedTuple - | Self::NewType => false, + | Self::NewType + | Self::Field => false, } } @@ -2526,7 +2533,8 @@ impl<'db> KnownClass { | Self::Super | Self::UnionType | Self::NamedTuple - | Self::NewType => false, + | Self::NewType + | Self::Field => false, } } @@ -2596,6 +2604,7 @@ impl<'db> KnownClass { Self::EllipsisType } "_NotImplementedType" => Self::NotImplementedType, + "Field" => Self::Field, _ => return None, }; @@ -2647,7 +2656,8 @@ impl<'db> KnownClass { | Self::UnionType | Self::GeneratorType | Self::AsyncGeneratorType - | Self::WrapperDescriptorType => module == self.canonical_module(db), + | Self::WrapperDescriptorType + | Self::Field => module == self.canonical_module(db), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), Self::SpecialForm | Self::TypeVar