From 99f077e65fe5a6dd67244e738513e909770c0b6f Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Bodas Date: Mon, 12 May 2025 00:41:27 +0530 Subject: [PATCH 1/6] [ty] Implement `DataClassInstance` protocol for dataclasses. --- .../resources/mdtest/dataclasses.md | 12 ++++++++++++ crates/ty_python_semantic/src/types.rs | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses.md index 54ac25998b388..28f929efc88f5 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses.md @@ -616,6 +616,18 @@ reveal_type(C.__init__) # revealed: (field: str | int = int) -> None To do +## `dataclass.fields` + +```py +from dataclasses import dataclass, fields + +@dataclass +class Foo: + x: int + +reveal_type(fields(Foo)) # revealed: @Todo(full tuple[...] support) +``` + ## Other special cases ### `dataclasses.dataclass` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 9e536cbf31f0b..8db24f2a7fa8d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2939,6 +2939,12 @@ 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(Type::any()).into() + } Type::BoundMethod(bound_method) => match name_str { "__self__" => Symbol::bound(bound_method.self_instance(db)).into(), "__func__" => { From 3156e854c2a9dbe1dcc8aa3da826206efab7d4ac Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Bodas Date: Mon, 12 May 2025 20:47:00 +0530 Subject: [PATCH 2/6] better type of __dataclass_fields__ --- .../ty_python_semantic/resources/mdtest/dataclasses.md | 7 +++++++ crates/ty_python_semantic/src/types.rs | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses.md index 28f929efc88f5..83f3ccd2a7ffc 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses.md @@ -618,6 +618,12 @@ 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 @@ -625,6 +631,7 @@ from dataclasses import dataclass, fields class Foo: x: int +reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, @Todo(dataclass.Field)] reveal_type(fields(Foo)) # revealed: @Todo(full tuple[...] support) ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 8db24f2a7fa8d..099d928f2d65d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2943,7 +2943,14 @@ impl<'db> Type<'db> { if name == "__dataclass_fields__" && class.dataclass_params(db).is_some() => { // Make this class look like a subclass of the `DataClassInstance` protocol - Symbol::bound(Type::any()).into() + Symbol::bound(KnownClass::Dict.to_specialized_instance( + db, + [ + KnownClass::Str.to_instance(db), + todo_type!("dataclass.Field"), + ], + )) + .with_qualifiers(TypeQualifiers::CLASS_VAR) } Type::BoundMethod(bound_method) => match name_str { "__self__" => Symbol::bound(bound_method.self_instance(db)).into(), From 18fb3facc77a636e8920b7e9182c3164351fdce7 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Bodas Date: Mon, 12 May 2025 22:38:31 +0530 Subject: [PATCH 3/6] add KnownClass:Field --- .../resources/mdtest/dataclasses.md | 2 +- crates/ty_python_semantic/src/types.rs | 2 +- crates/ty_python_semantic/src/types/class.rs | 20 ++++++++++++++----- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses.md index 83f3ccd2a7ffc..d19da495d4e8b 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses.md @@ -631,7 +631,7 @@ from dataclasses import dataclass, fields class Foo: x: int -reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, @Todo(dataclass.Field)] +reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, Field[Any]] reveal_type(fields(Foo)) # revealed: @Todo(full tuple[...] support) ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 099d928f2d65d..3eb740871c27e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2947,7 +2947,7 @@ impl<'db> Type<'db> { db, [ KnownClass::Str.to_instance(db), - todo_type!("dataclass.Field"), + KnownClass::Field.to_specialized_instance(db, [Type::any()]), ], )) .with_qualifiers(TypeQualifiers::CLASS_VAR) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 96673012f1f49..5aab478dcd861 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 { @@ -1996,7 +1998,8 @@ impl<'db> KnownClass { | Self::UnionType | Self::GeneratorType | Self::AsyncGeneratorType - | Self::MethodWrapperType => Truthiness::AlwaysTrue, + | Self::MethodWrapperType + | Self::Field => Truthiness::AlwaysTrue, Self::NoneType => Truthiness::AlwaysFalse, @@ -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, }; @@ -2659,7 +2668,8 @@ impl<'db> KnownClass { | Self::ParamSpecKwargs | Self::TypeVarTuple | Self::NamedTuple - | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), + | Self::NewType + | Self::Field => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), } } } From 70446c7a61fc40a9042ff3ddeb5dfd4985855088 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Bodas Date: Mon, 12 May 2025 22:58:45 +0530 Subject: [PATCH 4/6] fix tests --- crates/ty_python_semantic/src/types/class.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 5aab478dcd861..031bae93fcd74 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2656,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 @@ -2668,8 +2669,7 @@ impl<'db> KnownClass { | Self::ParamSpecKwargs | Self::TypeVarTuple | Self::NamedTuple - | Self::NewType - | Self::Field => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), + | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), } } } From 2b266f358e8e4ac7fbe60d901798c7a4fbba4efd Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Bodas Date: Mon, 12 May 2025 23:11:25 +0530 Subject: [PATCH 5/6] fix truthiness --- crates/ty_python_semantic/src/types/class.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 031bae93fcd74..0f7936a18d558 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1998,8 +1998,7 @@ impl<'db> KnownClass { | Self::UnionType | Self::GeneratorType | Self::AsyncGeneratorType - | Self::MethodWrapperType - | Self::Field => Truthiness::AlwaysTrue, + | Self::MethodWrapperType => Truthiness::AlwaysTrue, Self::NoneType => Truthiness::AlwaysFalse, @@ -2040,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, } } From b944ca132e64926d0b756f0d75b513eb824c3e67 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 13 May 2025 10:24:34 +0200 Subject: [PATCH 6/6] Update assertion --- crates/ty_python_semantic/resources/mdtest/dataclasses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses.md index d19da495d4e8b..7b5377d0132a3 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses.md @@ -632,7 +632,7 @@ class Foo: x: int reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, Field[Any]] -reveal_type(fields(Foo)) # revealed: @Todo(full tuple[...] support) +reveal_type(fields(Foo)) # revealed: tuple[Field[Any], ...] ``` ## Other special cases