mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[ty] Fix attribute access on TypedDict
s (#19758)
## Summary This PR fixes a few inaccuracies in attribute access on `TypedDict`s. It also changes the return type of `type(person)` to `type[dict[str, object]]` if `person: Person` is an inhabitant of a `TypedDict` `Person`. We still use `type[Person]` as the *meta type* of Person, however (see reasoning [here](https://github.com/astral-sh/ruff/pull/19733#discussion_r2253297926)). ## Test Plan Updated Markdown tests.
This commit is contained in:
parent
3af0b31de3
commit
948f3f856c
7 changed files with 139 additions and 60 deletions
|
@ -219,18 +219,27 @@ class Person(TypedDict):
|
|||
age: int | None
|
||||
|
||||
static_assert(not has_member(Person, "name"))
|
||||
static_assert(not has_member(Person, "age"))
|
||||
|
||||
static_assert(has_member(Person, "keys"))
|
||||
static_assert(has_member(Person, "__total__"))
|
||||
static_assert(has_member(Person, "__required_keys__"))
|
||||
|
||||
def _(person: Person):
|
||||
static_assert(not has_member(person, "name"))
|
||||
static_assert(not has_member(person, "age"))
|
||||
|
||||
static_assert(not has_member(person, "__total__"))
|
||||
static_assert(has_member(person, "keys"))
|
||||
|
||||
# type(person) is `dict` at runtime, so `__total__` is not available:
|
||||
static_assert(not has_member(type(person), "name"))
|
||||
static_assert(not has_member(type(person), "__total__"))
|
||||
static_assert(has_member(type(person), "keys"))
|
||||
|
||||
def _(t_person: type[Person]):
|
||||
static_assert(not has_member(t_person, "name"))
|
||||
static_assert(has_member(t_person, "__total__"))
|
||||
static_assert(has_member(t_person, "keys"))
|
||||
```
|
||||
|
||||
### Unions
|
||||
|
||||
For unions, `ide_support::all_members` only returns members that are available on all elements of
|
||||
the union.
|
||||
|
||||
|
|
|
@ -148,8 +148,8 @@ def _(p: Person) -> None:
|
|||
|
||||
## Unlike normal classes
|
||||
|
||||
`TypedDict` types are not like normal classes. The "attributes" can not be accessed. Neither on the
|
||||
class itself, nor on inhabitants of the type defined by the class:
|
||||
`TypedDict` types do not act like normal classes. For example, calling `type(..)` on an inhabitant
|
||||
of a `TypedDict` type will return `dict`:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
@ -158,6 +158,16 @@ class Person(TypedDict):
|
|||
name: str
|
||||
age: int | None
|
||||
|
||||
def _(p: Person) -> None:
|
||||
reveal_type(type(p)) # revealed: <class 'dict[str, object]'>
|
||||
|
||||
reveal_type(p.__class__) # revealed: <class 'dict[str, object]'>
|
||||
```
|
||||
|
||||
Also, the "attributes" on the class definition can not be accessed. Neither on the class itself, nor
|
||||
on inhabitants of the type defined by the class:
|
||||
|
||||
```py
|
||||
# error: [unresolved-attribute] "Type `<class 'Person'>` has no attribute `name`"
|
||||
Person.name
|
||||
|
||||
|
@ -168,6 +178,8 @@ def _(P: type[Person]):
|
|||
def _(p: Person) -> None:
|
||||
# error: [unresolved-attribute] "Type `Person` has no attribute `name`"
|
||||
p.name
|
||||
|
||||
type(p).name # error: [unresolved-attribute] "Type `<class 'dict[str, object]'>` has no attribute `name`"
|
||||
```
|
||||
|
||||
## Special properties
|
||||
|
@ -190,20 +202,30 @@ These attributes can not be accessed on inhabitants:
|
|||
|
||||
```py
|
||||
def _(person: Person) -> None:
|
||||
# TODO: these should be errors
|
||||
person.__total__
|
||||
person.__required_keys__
|
||||
person.__optional_keys__
|
||||
person.__total__ # error: [unresolved-attribute]
|
||||
person.__required_keys__ # error: [unresolved-attribute]
|
||||
person.__optional_keys__ # error: [unresolved-attribute]
|
||||
```
|
||||
|
||||
Also, they can not be accessed on `type(person)`, as that would be `dict` at runtime:
|
||||
|
||||
```py
|
||||
def _(t_person: type[Person]) -> None:
|
||||
# TODO: these should be errors
|
||||
t_person.__total__
|
||||
t_person.__required_keys__
|
||||
t_person.__optional_keys__
|
||||
def _(person: Person) -> None:
|
||||
type(person).__total__ # error: [unresolved-attribute]
|
||||
type(person).__required_keys__ # error: [unresolved-attribute]
|
||||
type(person).__optional_keys__ # error: [unresolved-attribute]
|
||||
```
|
||||
|
||||
But they *can* be accessed on `type[Person]`, because this function would accept the class object
|
||||
`Person` as an argument:
|
||||
|
||||
```py
|
||||
def accepts_typed_dict_class(t_person: type[Person]) -> None:
|
||||
reveal_type(t_person.__total__) # revealed: bool
|
||||
reveal_type(t_person.__required_keys__) # revealed: frozenset[str]
|
||||
reveal_type(t_person.__optional_keys__) # revealed: frozenset[str]
|
||||
|
||||
accepts_typed_dict_class(Person)
|
||||
```
|
||||
|
||||
## Subclassing
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue