[ty] emit diagnostics for method definitions and other invalid statements in TypedDict class bodies (#22351)
Fixes https://github.com/astral-sh/ty/issues/2277.
This commit is contained in:
@@ -551,10 +551,8 @@ class MyNamedTupleChild(MyNamedTupleParent):
|
||||
class MyTypedDict(TypedDict):
|
||||
x: int
|
||||
|
||||
# error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
|
||||
@override
|
||||
# TODO: it's invalid to define a method on a `TypedDict` class,
|
||||
# so we should emit a diagnostic here.
|
||||
# It shouldn't be an `invalid-explicit-override` diagnostic, however.
|
||||
def copy(self) -> Self: ...
|
||||
|
||||
class Grandparent(Any): ...
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
|
||||
---
|
||||
mdtest name: typed_dict.md - `TypedDict` - Only annotated declarations are allowed in the class body
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import TypedDict
|
||||
2 |
|
||||
3 | class Foo(TypedDict):
|
||||
4 | """docstring"""
|
||||
5 |
|
||||
6 | annotated_item: int
|
||||
7 | """attribute docstring"""
|
||||
8 |
|
||||
9 | pass
|
||||
10 |
|
||||
11 | # As a non-standard but common extension, we interpret `...` as equivalent to `pass`.
|
||||
12 | ...
|
||||
13 |
|
||||
14 | class Bar(TypedDict):
|
||||
15 | a: int
|
||||
16 | # error: [invalid-typed-dict-statement] "invalid statement in TypedDict class body"
|
||||
17 | 42
|
||||
18 | # error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
|
||||
19 | b: str = "hello"
|
||||
20 | # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
|
||||
21 | def bar(self): ...
|
||||
22 | class Baz(Bar):
|
||||
23 | # error: [invalid-typed-dict-statement]
|
||||
24 | def baz(self):
|
||||
25 | pass
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-typed-dict-statement]: invalid statement in TypedDict class body
|
||||
--> src/mdtest_snippet.py:17:5
|
||||
|
|
||||
15 | a: int
|
||||
16 | # error: [invalid-typed-dict-statement] "invalid statement in TypedDict class body"
|
||||
17 | 42
|
||||
| ^^
|
||||
18 | # error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
|
||||
19 | b: str = "hello"
|
||||
|
|
||||
info: Only annotated declarations (`<name>: <type>`) are allowed.
|
||||
info: rule `invalid-typed-dict-statement` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-typed-dict-statement]: TypedDict item cannot have a value
|
||||
--> src/mdtest_snippet.py:19:14
|
||||
|
|
||||
17 | 42
|
||||
18 | # error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
|
||||
19 | b: str = "hello"
|
||||
| ^^^^^^^
|
||||
20 | # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
|
||||
21 | def bar(self): ...
|
||||
|
|
||||
info: rule `invalid-typed-dict-statement` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-typed-dict-statement]: TypedDict class cannot have methods
|
||||
--> src/mdtest_snippet.py:21:5
|
||||
|
|
||||
19 | b: str = "hello"
|
||||
20 | # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
|
||||
21 | def bar(self): ...
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
22 | class Baz(Bar):
|
||||
23 | # error: [invalid-typed-dict-statement]
|
||||
|
|
||||
info: rule `invalid-typed-dict-statement` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-typed-dict-statement]: TypedDict class cannot have methods
|
||||
--> src/mdtest_snippet.py:24:5
|
||||
|
|
||||
22 | class Baz(Bar):
|
||||
23 | # error: [invalid-typed-dict-statement]
|
||||
24 | / def baz(self):
|
||||
25 | | pass
|
||||
| |____________^
|
||||
|
|
||||
info: rule `invalid-typed-dict-statement` is enabled by default
|
||||
|
||||
```
|
||||
@@ -2266,6 +2266,47 @@ def match_with_dict(u: Foo | Bar | dict):
|
||||
reveal_type(u) # revealed: Foo | (dict[Unknown, Unknown] & ~<TypedDict with items 'tag'>)
|
||||
```
|
||||
|
||||
## Only annotated declarations are allowed in the class body
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`TypedDict` class bodies are very restricted in what kinds of statements they can contain. Besides
|
||||
annotated items, the only allowed statements are docstrings and `pass`. Annotated items are are also
|
||||
not allowed to have a value.
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class Foo(TypedDict):
|
||||
"""docstring"""
|
||||
|
||||
annotated_item: int
|
||||
"""attribute docstring"""
|
||||
|
||||
pass
|
||||
|
||||
# As a non-standard but common extension, we interpret `...` as equivalent to `pass`.
|
||||
...
|
||||
|
||||
class Bar(TypedDict):
|
||||
a: int
|
||||
# error: [invalid-typed-dict-statement] "invalid statement in TypedDict class body"
|
||||
42
|
||||
# error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
|
||||
b: str = "hello"
|
||||
# error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
|
||||
def bar(self): ...
|
||||
```
|
||||
|
||||
These rules are also enforced for `TypedDict` classes that don't directly inherit from `TypedDict`:
|
||||
|
||||
```py
|
||||
class Baz(Bar):
|
||||
# error: [invalid-typed-dict-statement]
|
||||
def baz(self):
|
||||
pass
|
||||
```
|
||||
|
||||
[closed]: https://peps.python.org/pep-0728/#disallowing-extra-items-explicitly
|
||||
[subtyping section]: https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types
|
||||
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html
|
||||
|
||||
Reference in New Issue
Block a user