Skip to content

Commit f521358

Browse files
authored
[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 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
1 parent 7408103 commit f521358

File tree

10 files changed

+84
-9
lines changed

10 files changed

+84
-9
lines changed

crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,12 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
3838
```py
3939
from typing_extensions import LiteralString
4040

41-
a: LiteralString[str] # error: [invalid-type-form]
42-
b: LiteralString["foo"] # error: [invalid-type-form]
41+
# error: [invalid-type-form]
42+
a: LiteralString[str]
43+
44+
# error: [invalid-type-form]
45+
# error: [unresolved-reference] "Name `foo` used when not defined"
46+
b: LiteralString["foo"]
4347
```
4448

4549
### As a base class

crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,12 @@ python-version = "3.12"
8989
Some of these are not subscriptable:
9090

9191
```py
92-
from typing_extensions import Self, TypeAlias
92+
from typing_extensions import Self, TypeAlias, TypeVar
9393

94-
X: TypeAlias[T] = int # error: [invalid-type-form]
94+
T = TypeVar("T")
95+
96+
# error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter"
97+
X: TypeAlias[T] = int
9598

9699
class Foo[T]:
97100
# error: [invalid-type-form] "Special form `typing.Self` expected no type parameter"

crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict
1111
X: Final = 42
1212
Y: Final[int] = 42
1313

14-
# TODO: `TypedDict` is actually valid as a base
15-
# error: [invalid-base]
1614
class Bar(TypedDict):
1715
x: Required[int]
1816
y: NotRequired[str]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# `TypedDict`
2+
3+
We do not support `TypedDict`s yet. This test mainly exists to make sure that we do not emit any
4+
errors for the definition of a `TypedDict`.
5+
6+
```py
7+
from typing_extensions import TypedDict, Required
8+
9+
class Person(TypedDict):
10+
name: str
11+
age: int | None
12+
13+
# TODO: This should not be an error:
14+
# error: [invalid-assignment]
15+
alice: Person = {"name": "Alice", "age": 30}
16+
17+
# Alternative syntax
18+
Message = TypedDict("Message", {"id": Required[int], "content": str}, total=False)
19+
20+
msg = Message(id=1, content="Hello")
21+
22+
# No errors for yet-unsupported features (`closed`):
23+
OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True)
24+
```

crates/red_knot_python_semantic/src/types.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3890,6 +3890,28 @@ impl<'db> Type<'db> {
38903890
}
38913891
},
38923892

3893+
Type::KnownInstance(KnownInstanceType::TypedDict) => {
3894+
Signatures::single(CallableSignature::single(
3895+
self,
3896+
Signature::new(
3897+
Parameters::new([
3898+
Parameter::positional_only(Some(Name::new_static("typename")))
3899+
.with_annotated_type(KnownClass::Str.to_instance(db)),
3900+
Parameter::positional_only(Some(Name::new_static("fields")))
3901+
.with_annotated_type(KnownClass::Dict.to_instance(db))
3902+
.with_default_type(Type::any()),
3903+
Parameter::keyword_only(Name::new_static("total"))
3904+
.with_annotated_type(KnownClass::Bool.to_instance(db))
3905+
.with_default_type(Type::BooleanLiteral(true)),
3906+
// Future compatibility, in case new keyword arguments will be added:
3907+
Parameter::keyword_variadic(Name::new_static("kwargs"))
3908+
.with_annotated_type(Type::any()),
3909+
]),
3910+
None,
3911+
),
3912+
))
3913+
}
3914+
38933915
Type::GenericAlias(_) => {
38943916
// TODO annotated return type on `__new__` or metaclass `__call__`
38953917
// TODO check call vs signatures of `__new__` and/or `__init__`
@@ -4471,6 +4493,7 @@ impl<'db> Type<'db> {
44714493

44724494
KnownInstanceType::TypingSelf => Ok(todo_type!("Support for `typing.Self`")),
44734495
KnownInstanceType::TypeAlias => Ok(todo_type!("Support for `typing.TypeAlias`")),
4496+
KnownInstanceType::TypedDict => Ok(todo_type!("Support for `typing.TypedDict`")),
44744497

44754498
KnownInstanceType::Protocol => Err(InvalidTypeExpressionError {
44764499
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol],

crates/red_knot_python_semantic/src/types/call/bind.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ use crate::types::diagnostic::{
1919
use crate::types::generics::{Specialization, SpecializationBuilder};
2020
use crate::types::signatures::{Parameter, ParameterForm};
2121
use crate::types::{
22-
BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, KnownClass,
23-
KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, TupleType,
24-
UnionType, WrapperDescriptorKind,
22+
todo_type, BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators,
23+
KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType,
24+
TupleType, UnionType, WrapperDescriptorKind,
2525
};
2626
use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic};
2727
use ruff_python_ast as ast;
@@ -772,6 +772,10 @@ impl<'db> Bindings<'db> {
772772
_ => {}
773773
},
774774

775+
Type::KnownInstance(KnownInstanceType::TypedDict) => {
776+
overload.set_return_type(todo_type!("TypedDict"));
777+
}
778+
775779
// Not a special case
776780
_ => {}
777781
}

crates/red_knot_python_semantic/src/types/class_base.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ impl<'db> ClassBase<'db> {
169169
KnownInstanceType::OrderedDict => {
170170
Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db))
171171
}
172+
KnownInstanceType::TypedDict => {
173+
Self::try_from_type(db, KnownClass::Dict.to_class_literal(db))
174+
}
172175
KnownInstanceType::Callable => {
173176
Self::try_from_type(db, todo_type!("Support for Callable as a base class"))
174177
}

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7710,6 +7710,8 @@ impl<'db> TypeInferenceBuilder<'db> {
77107710
| KnownInstanceType::Any
77117711
| KnownInstanceType::AlwaysTruthy
77127712
| KnownInstanceType::AlwaysFalsy => {
7713+
self.infer_type_expression(arguments_slice);
7714+
77137715
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
77147716
builder.into_diagnostic(format_args!(
77157717
"Type `{}` expected no type parameter",
@@ -7720,7 +7722,10 @@ impl<'db> TypeInferenceBuilder<'db> {
77207722
}
77217723
KnownInstanceType::TypingSelf
77227724
| KnownInstanceType::TypeAlias
7725+
| KnownInstanceType::TypedDict
77237726
| KnownInstanceType::Unknown => {
7727+
self.infer_type_expression(arguments_slice);
7728+
77247729
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
77257730
builder.into_diagnostic(format_args!(
77267731
"Special form `{}` expected no type parameter",
@@ -7730,6 +7735,8 @@ impl<'db> TypeInferenceBuilder<'db> {
77307735
Type::unknown()
77317736
}
77327737
KnownInstanceType::LiteralString => {
7738+
self.infer_type_expression(arguments_slice);
7739+
77337740
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
77347741
let mut diag = builder.into_diagnostic(format_args!(
77357742
"Type `{}` expected no type parameter",

crates/red_knot_python_semantic/src/types/known_instance.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ pub enum KnownInstanceType<'db> {
9595
NotRequired,
9696
TypeAlias,
9797
TypeGuard,
98+
TypedDict,
9899
TypeIs,
99100
ReadOnly,
100101
// TODO: fill this enum out with more special forms, etc.
@@ -125,6 +126,7 @@ impl<'db> KnownInstanceType<'db> {
125126
| Self::NotRequired
126127
| Self::TypeAlias
127128
| Self::TypeGuard
129+
| Self::TypedDict
128130
| Self::TypeIs
129131
| Self::List
130132
| Self::Dict
@@ -172,6 +174,7 @@ impl<'db> KnownInstanceType<'db> {
172174
Self::NotRequired => "typing.NotRequired",
173175
Self::TypeAlias => "typing.TypeAlias",
174176
Self::TypeGuard => "typing.TypeGuard",
177+
Self::TypedDict => "typing.TypedDict",
175178
Self::TypeIs => "typing.TypeIs",
176179
Self::List => "typing.List",
177180
Self::Dict => "typing.Dict",
@@ -220,6 +223,7 @@ impl<'db> KnownInstanceType<'db> {
220223
Self::NotRequired => KnownClass::SpecialForm,
221224
Self::TypeAlias => KnownClass::SpecialForm,
222225
Self::TypeGuard => KnownClass::SpecialForm,
226+
Self::TypedDict => KnownClass::SpecialForm,
223227
Self::TypeIs => KnownClass::SpecialForm,
224228
Self::ReadOnly => KnownClass::SpecialForm,
225229
Self::List => KnownClass::StdlibAlias,
@@ -293,6 +297,7 @@ impl<'db> KnownInstanceType<'db> {
293297
"Required" => Self::Required,
294298
"TypeAlias" => Self::TypeAlias,
295299
"TypeGuard" => Self::TypeGuard,
300+
"TypedDict" => Self::TypedDict,
296301
"TypeIs" => Self::TypeIs,
297302
"ReadOnly" => Self::ReadOnly,
298303
"Concatenate" => Self::Concatenate,
@@ -350,6 +355,7 @@ impl<'db> KnownInstanceType<'db> {
350355
| Self::NotRequired
351356
| Self::TypeAlias
352357
| Self::TypeGuard
358+
| Self::TypedDict
353359
| Self::TypeIs
354360
| Self::ReadOnly
355361
| Self::TypeAliasType(_)

crates/red_knot_python_semantic/src/types/type_ordering.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
221221
(KnownInstanceType::TypeGuard, _) => Ordering::Less,
222222
(_, KnownInstanceType::TypeGuard) => Ordering::Greater,
223223

224+
(KnownInstanceType::TypedDict, _) => Ordering::Less,
225+
(_, KnownInstanceType::TypedDict) => Ordering::Greater,
226+
224227
(KnownInstanceType::List, _) => Ordering::Less,
225228
(_, KnownInstanceType::List) => Ordering::Greater,
226229

0 commit comments

Comments
 (0)