[ty] Add synthetic members to completions on dataclasses (#21446)

## Summary

Add synthetic members to completions on dataclasses and dataclass
instances.

Also, while we're at it, add support for `__weakref__` and
`__match_args__`.

closes https://github.com/astral-sh/ty/issues/1542

## Test Plan

New Markdown tests
This commit is contained in:
David Peter 2025-11-14 11:31:20 +01:00 committed by GitHub
parent 66e9d57797
commit 696d7a5d68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 342 additions and 39 deletions

View file

@ -461,7 +461,51 @@ del frozen.x # TODO this should emit an [invalid-assignment]
### `match_args`
To do
If `match_args` is set to `True` (the default), the `__match_args__` attribute is a tuple created
from the list of non keyword-only parameters to the synthesized `__init__` method (even if
`__init__` is not actually generated).
```py
from dataclasses import dataclass, field
@dataclass
class WithMatchArgs:
normal_a: str
normal_b: int
kw_only: int = field(kw_only=True)
reveal_type(WithMatchArgs.__match_args__) # revealed: tuple[Literal["normal_a"], Literal["normal_b"]]
@dataclass(kw_only=True)
class KwOnlyDefaultMatchArgs:
normal_a: str = field(kw_only=False)
normal_b: int = field(kw_only=False)
kw_only: int
reveal_type(KwOnlyDefaultMatchArgs.__match_args__) # revealed: tuple[Literal["normal_a"], Literal["normal_b"]]
@dataclass(match_args=True)
class ExplicitMatchArgs:
normal: str
reveal_type(ExplicitMatchArgs.__match_args__) # revealed: tuple[Literal["normal"]]
@dataclass
class Empty: ...
reveal_type(Empty.__match_args__) # revealed: tuple[()]
```
When `match_args` is explicitly set to `False`, the `__match_args__` attribute is not available:
```py
@dataclass(match_args=False)
class NoMatchArgs:
x: int
y: str
NoMatchArgs.__match_args__ # error: [unresolved-attribute]
```
### `kw_only`
@ -623,7 +667,18 @@ reveal_type(B.__slots__) # revealed: tuple[Literal["x"], Literal["y"]]
### `weakref_slot`
To do
When a dataclass is defined with `weakref_slot=True`, the `__weakref__` attribute is generated. For
now, we do not attempt to infer a more precise type for it.
```py
from dataclasses import dataclass
@dataclass(slots=True, weakref_slot=True)
class C:
x: int
reveal_type(C.__weakref__) # revealed: Any | None
```
## `Final` fields

View file

@ -548,13 +548,20 @@ static_assert(not has_member(c, "dynamic_attr"))
### Dataclasses
So far, we do not include synthetic members of dataclasses.
#### Basic
For dataclasses, we make sure to include all synthesized members:
```toml
[environment]
python-version = "3.9"
```
```py
from ty_extensions import has_member, static_assert
from dataclasses import dataclass
@dataclass(order=True)
@dataclass
class Person:
age: int
name: str
@ -562,13 +569,177 @@ class Person:
static_assert(has_member(Person, "name"))
static_assert(has_member(Person, "age"))
static_assert(has_member(Person, "__dataclass_fields__"))
static_assert(has_member(Person, "__dataclass_params__"))
# These are always available, since they are also defined on `object`:
static_assert(has_member(Person, "__init__"))
static_assert(has_member(Person, "__repr__"))
static_assert(has_member(Person, "__eq__"))
static_assert(has_member(Person, "__ne__"))
# TODO: this should ideally be available:
static_assert(has_member(Person, "__lt__")) # error: [static-assert-error]
# There are not available, unless `order=True` is set:
static_assert(not has_member(Person, "__lt__"))
static_assert(not has_member(Person, "__le__"))
static_assert(not has_member(Person, "__gt__"))
static_assert(not has_member(Person, "__ge__"))
# These are not available, unless `slots=True`, `weakref_slot=True` are set:
static_assert(not has_member(Person, "__slots__"))
static_assert(not has_member(Person, "__weakref__"))
# Not available before Python 3.13:
static_assert(not has_member(Person, "__replace__"))
```
The same behavior applies to instances of dataclasses:
```py
def _(person: Person):
static_assert(has_member(person, "name"))
static_assert(has_member(person, "age"))
static_assert(has_member(person, "__dataclass_fields__"))
static_assert(has_member(person, "__dataclass_params__"))
static_assert(has_member(person, "__init__"))
static_assert(has_member(person, "__repr__"))
static_assert(has_member(person, "__eq__"))
static_assert(has_member(person, "__ne__"))
static_assert(not has_member(person, "__lt__"))
static_assert(not has_member(person, "__le__"))
static_assert(not has_member(person, "__gt__"))
static_assert(not has_member(person, "__ge__"))
static_assert(not has_member(person, "__slots__"))
static_assert(not has_member(person, "__replace__"))
```
#### `__init__`, `__repr__` and `__eq__`
`__init__`, `__repr__` and `__eq__` are always available (via `object`), even when `init=False`,
`repr=False` and `eq=False` are set:
```py
from ty_extensions import has_member, static_assert
from dataclasses import dataclass
@dataclass(init=False, repr=False, eq=False)
class C:
x: int
static_assert(has_member(C, "__init__"))
static_assert(has_member(C, "__repr__"))
static_assert(has_member(C, "__eq__"))
static_assert(has_member(C, "__ne__"))
static_assert(has_member(C(), "__init__"))
static_assert(has_member(C(), "__repr__"))
static_assert(has_member(C(), "__eq__"))
static_assert(has_member(C(), "__ne__"))
```
#### `order=True`
When `order=True` is set, comparison dunder methods become available:
```py
from ty_extensions import has_member, static_assert
from dataclasses import dataclass
@dataclass(order=True)
class C:
x: int
static_assert(has_member(C, "__lt__"))
static_assert(has_member(C, "__le__"))
static_assert(has_member(C, "__gt__"))
static_assert(has_member(C, "__ge__"))
def _(c: C):
static_assert(has_member(c, "__lt__"))
static_assert(has_member(c, "__le__"))
static_assert(has_member(c, "__gt__"))
static_assert(has_member(c, "__ge__"))
```
#### `slots=True`
When `slots=True`, the corresponding dunder attribute becomes available:
```py
from ty_extensions import has_member, static_assert
from dataclasses import dataclass
@dataclass(slots=True)
class C:
x: int
static_assert(has_member(C, "__slots__"))
static_assert(has_member(C(1), "__slots__"))
```
#### `weakref_slot=True`
When `weakref_slot=True`, the corresponding dunder attribute becomes available:
```py
from ty_extensions import has_member, static_assert
from dataclasses import dataclass
@dataclass(slots=True, weakref_slot=True)
class C:
x: int
static_assert(has_member(C, "__weakref__"))
static_assert(has_member(C(1), "__weakref__"))
```
#### `__replace__` in Python 3.13+
Since Python 3.13, dataclasses have a `__replace__` method:
```toml
[environment]
python-version = "3.13"
```
```py
from ty_extensions import has_member, static_assert
from dataclasses import dataclass
@dataclass
class C:
x: int
static_assert(has_member(C, "__replace__"))
def _(c: C):
static_assert(has_member(c, "__replace__"))
```
#### `__match_args__`
Since Python 3.10, dataclasses have a `__match_args__` attribute:
```toml
[environment]
python-version = "3.10"
```
```py
from ty_extensions import has_member, static_assert
from dataclasses import dataclass
@dataclass
class C:
x: int
static_assert(has_member(C, "__match_args__"))
def _(c: C):
static_assert(has_member(c, "__match_args__"))
```
### Attributes not available at runtime