From f521358033aa38fcd33b8a00cd1082bd836e4844 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 28 Apr 2025 13:13:28 +0200 Subject: [PATCH] [red-knot] No errors for definitions of `TypedDict`s (#17674) ## Summary Do not emit errors when defining `TypedDict`s: ```py from typing_extensions import TypedDict # No error here class Person(TypedDict): name: str age: int | None # No error for this alternative syntax Message = TypedDict("Message", {"id": int, "content": str}) ``` ## Ecosystem analysis * Removes ~ 450 false positives for `TypedDict` definitions. * Changes a few diagnostic messages. * Adds a few (< 10) false positives, for example: ```diff + error[lint:unresolved-attribute] /tmp/mypy_primer/projects/hydra-zen/src/hydra_zen/structured_configs/_utils.py:262:5: Type `Literal[DataclassOptions]` has no attribute `__required_keys__` + error[lint:unresolved-attribute] /tmp/mypy_primer/projects/hydra-zen/src/hydra_zen/structured_configs/_utils.py:262:42: Type `Literal[DataclassOptions]` has no attribute `__optional_keys__` ``` * New true positive https://github.com/zulip/zulip/blob/4f8263cd7f4d00fc9b9e7d687ab98b0cc8737308/corporate/lib/remote_billing_util.py#L155-L157 ```diff + error[lint:invalid-assignment] /tmp/mypy_primer/projects/zulip/corporate/lib/remote_billing_util.py:155:5: Object of type `RemoteBillingIdentityDict | LegacyServerIdentityDict | None` is not assignable to `LegacyServerIdentityDict | None` ``` ## Test Plan New Markdown tests --- .../mdtest/annotations/literal_string.md | 8 +++++-- .../annotations/unsupported_special_forms.md | 7 ++++-- .../unsupported_type_qualifiers.md | 2 -- .../resources/mdtest/typed_dict.md | 24 +++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 23 ++++++++++++++++++ .../src/types/call/bind.rs | 10 +++++--- .../src/types/class_base.rs | 3 +++ .../src/types/infer.rs | 7 ++++++ .../src/types/known_instance.rs | 6 +++++ .../src/types/type_ordering.rs | 3 +++ 10 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/typed_dict.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md index 445c7a05da..0496dbb4ce 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md @@ -38,8 +38,12 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form] ```py from typing_extensions import LiteralString -a: LiteralString[str] # error: [invalid-type-form] -b: LiteralString["foo"] # error: [invalid-type-form] +# error: [invalid-type-form] +a: LiteralString[str] + +# error: [invalid-type-form] +# error: [unresolved-reference] "Name `foo` used when not defined" +b: LiteralString["foo"] ``` ### As a base class diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 3d76ece35d..1c7665d84d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -89,9 +89,12 @@ python-version = "3.12" Some of these are not subscriptable: ```py -from typing_extensions import Self, TypeAlias +from typing_extensions import Self, TypeAlias, TypeVar -X: TypeAlias[T] = int # error: [invalid-type-form] +T = TypeVar("T") + +# error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter" +X: TypeAlias[T] = int class Foo[T]: # error: [invalid-type-form] "Special form `typing.Self` expected no type parameter" diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md index 7826fbf92d..fcc61160f2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md @@ -11,8 +11,6 @@ from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict X: Final = 42 Y: Final[int] = 42 -# TODO: `TypedDict` is actually valid as a base -# error: [invalid-base] class Bar(TypedDict): x: Required[int] y: NotRequired[str] diff --git a/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md b/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md new file mode 100644 index 0000000000..22d43260ba --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md @@ -0,0 +1,24 @@ +# `TypedDict` + +We do not support `TypedDict`s yet. This test mainly exists to make sure that we do not emit any +errors for the definition of a `TypedDict`. + +```py +from typing_extensions import TypedDict, Required + +class Person(TypedDict): + name: str + age: int | None + +# TODO: This should not be an error: +# error: [invalid-assignment] +alice: Person = {"name": "Alice", "age": 30} + +# Alternative syntax +Message = TypedDict("Message", {"id": Required[int], "content": str}, total=False) + +msg = Message(id=1, content="Hello") + +# No errors for yet-unsupported features (`closed`): +OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True) +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 070adc5aab..9a86e1a641 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3890,6 +3890,28 @@ impl<'db> Type<'db> { } }, + Type::KnownInstance(KnownInstanceType::TypedDict) => { + Signatures::single(CallableSignature::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("typename"))) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("fields"))) + .with_annotated_type(KnownClass::Dict.to_instance(db)) + .with_default_type(Type::any()), + Parameter::keyword_only(Name::new_static("total")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + // Future compatibility, in case new keyword arguments will be added: + Parameter::keyword_variadic(Name::new_static("kwargs")) + .with_annotated_type(Type::any()), + ]), + None, + ), + )) + } + Type::GenericAlias(_) => { // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` @@ -4471,6 +4493,7 @@ impl<'db> Type<'db> { KnownInstanceType::TypingSelf => Ok(todo_type!("Support for `typing.Self`")), KnownInstanceType::TypeAlias => Ok(todo_type!("Support for `typing.TypeAlias`")), + KnownInstanceType::TypedDict => Ok(todo_type!("Support for `typing.TypedDict`")), KnownInstanceType::Protocol => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol], diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 397c2aaa11..8f5fb66658 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -19,9 +19,9 @@ use crate::types::diagnostic::{ use crate::types::generics::{Specialization, SpecializationBuilder}; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ - BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, KnownClass, - KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, TupleType, - UnionType, WrapperDescriptorKind, + todo_type, BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, + KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, + TupleType, UnionType, WrapperDescriptorKind, }; use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic}; use ruff_python_ast as ast; @@ -772,6 +772,10 @@ impl<'db> Bindings<'db> { _ => {} }, + Type::KnownInstance(KnownInstanceType::TypedDict) => { + overload.set_return_type(todo_type!("TypedDict")); + } + // Not a special case _ => {} } diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 395ff5268d..91c132fc3d 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -169,6 +169,9 @@ impl<'db> ClassBase<'db> { KnownInstanceType::OrderedDict => { Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db)) } + KnownInstanceType::TypedDict => { + Self::try_from_type(db, KnownClass::Dict.to_class_literal(db)) + } KnownInstanceType::Callable => { Self::try_from_type(db, todo_type!("Support for Callable as a base class")) } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 29d69e5099..e9448d83b0 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -7710,6 +7710,8 @@ impl<'db> TypeInferenceBuilder<'db> { | KnownInstanceType::Any | KnownInstanceType::AlwaysTruthy | KnownInstanceType::AlwaysFalsy => { + self.infer_type_expression(arguments_slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Type `{}` expected no type parameter", @@ -7720,7 +7722,10 @@ impl<'db> TypeInferenceBuilder<'db> { } KnownInstanceType::TypingSelf | KnownInstanceType::TypeAlias + | KnownInstanceType::TypedDict | KnownInstanceType::Unknown => { + self.infer_type_expression(arguments_slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Special form `{}` expected no type parameter", @@ -7730,6 +7735,8 @@ impl<'db> TypeInferenceBuilder<'db> { Type::unknown() } KnownInstanceType::LiteralString => { + self.infer_type_expression(arguments_slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { let mut diag = builder.into_diagnostic(format_args!( "Type `{}` expected no type parameter", diff --git a/crates/red_knot_python_semantic/src/types/known_instance.rs b/crates/red_knot_python_semantic/src/types/known_instance.rs index 27ed7a533a..01316c1c56 100644 --- a/crates/red_knot_python_semantic/src/types/known_instance.rs +++ b/crates/red_knot_python_semantic/src/types/known_instance.rs @@ -95,6 +95,7 @@ pub enum KnownInstanceType<'db> { NotRequired, TypeAlias, TypeGuard, + TypedDict, TypeIs, ReadOnly, // TODO: fill this enum out with more special forms, etc. @@ -125,6 +126,7 @@ impl<'db> KnownInstanceType<'db> { | Self::NotRequired | Self::TypeAlias | Self::TypeGuard + | Self::TypedDict | Self::TypeIs | Self::List | Self::Dict @@ -172,6 +174,7 @@ impl<'db> KnownInstanceType<'db> { Self::NotRequired => "typing.NotRequired", Self::TypeAlias => "typing.TypeAlias", Self::TypeGuard => "typing.TypeGuard", + Self::TypedDict => "typing.TypedDict", Self::TypeIs => "typing.TypeIs", Self::List => "typing.List", Self::Dict => "typing.Dict", @@ -220,6 +223,7 @@ impl<'db> KnownInstanceType<'db> { Self::NotRequired => KnownClass::SpecialForm, Self::TypeAlias => KnownClass::SpecialForm, Self::TypeGuard => KnownClass::SpecialForm, + Self::TypedDict => KnownClass::SpecialForm, Self::TypeIs => KnownClass::SpecialForm, Self::ReadOnly => KnownClass::SpecialForm, Self::List => KnownClass::StdlibAlias, @@ -293,6 +297,7 @@ impl<'db> KnownInstanceType<'db> { "Required" => Self::Required, "TypeAlias" => Self::TypeAlias, "TypeGuard" => Self::TypeGuard, + "TypedDict" => Self::TypedDict, "TypeIs" => Self::TypeIs, "ReadOnly" => Self::ReadOnly, "Concatenate" => Self::Concatenate, @@ -350,6 +355,7 @@ impl<'db> KnownInstanceType<'db> { | Self::NotRequired | Self::TypeAlias | Self::TypeGuard + | Self::TypedDict | Self::TypeIs | Self::ReadOnly | Self::TypeAliasType(_) diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 8037611f51..64d9bb726a 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -221,6 +221,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (KnownInstanceType::TypeGuard, _) => Ordering::Less, (_, KnownInstanceType::TypeGuard) => Ordering::Greater, + (KnownInstanceType::TypedDict, _) => Ordering::Less, + (_, KnownInstanceType::TypedDict) => Ordering::Greater, + (KnownInstanceType::List, _) => Ordering::Less, (_, KnownInstanceType::List) => Ordering::Greater,