[red-knot] No errors for definitions of TypedDicts (#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

4f8263cd7f/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
This commit is contained in:
David Peter 2025-04-28 13:13:28 +02:00 committed by GitHub
parent 74081032d9
commit f521358033
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 84 additions and 9 deletions

View file

@ -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

View file

@ -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"

View file

@ -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]

View file

@ -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)
```

View file

@ -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],

View file

@ -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
_ => {}
}

View file

@ -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"))
}

View file

@ -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",

View file

@ -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(_)

View file

@ -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,