Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,27 +548,198 @@ 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

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__"))

# 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__"))

# TODO: this should ideally be available:
static_assert(has_member(Person, "__lt__")) # error: [static-assert-error]
def _(c: C):
static_assert(has_member(c, "__match_args__"))
```

### Attributes not available at runtime
Expand Down
64 changes: 52 additions & 12 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2122,18 +2122,25 @@ impl<'db> ClassLiteral<'db> {
specialization: Option<Specialization<'db>>,
name: &str,
) -> Member<'db> {
if name == "__dataclass_fields__" && self.dataclass_params(db).is_some() {
// Make this class look like a subclass of the `DataClassInstance` protocol
return Member {
inner: Place::declared(KnownClass::Dict.to_specialized_instance(
db,
[
KnownClass::Str.to_instance(db),
KnownClass::Field.to_specialized_instance(db, [Type::any()]),
],
))
.with_qualifiers(TypeQualifiers::CLASS_VAR),
};
if self.dataclass_params(db).is_some() {
if name == "__dataclass_fields__" {
// Make this class look like a subclass of the `DataClassInstance` protocol
return Member {
inner: Place::declared(KnownClass::Dict.to_specialized_instance(
db,
[
KnownClass::Str.to_instance(db),
KnownClass::Field.to_specialized_instance(db, [Type::any()]),
],
))
.with_qualifiers(TypeQualifiers::CLASS_VAR),
};
} else if name == "__dataclass_params__" {
// There is no typeshed class for this. For now, we model it as `Any`.
return Member {
inner: Place::declared(Type::any()).with_qualifiers(TypeQualifiers::CLASS_VAR),
};
}
}

if CodeGeneratorKind::NamedTuple.matches(db, self, specialization) {
Expand Down Expand Up @@ -2368,6 +2375,39 @@ impl<'db> ClassLiteral<'db> {

Some(CallableType::function_like(db, signature))
}
(CodeGeneratorKind::DataclassLike(_), "__match_args__")
if Program::get(db).python_version(db) >= PythonVersion::PY310 =>
{
if !has_dataclass_param(DataclassFlags::MATCH_ARGS) {
return None;
}

let kw_only_default = has_dataclass_param(DataclassFlags::KW_ONLY);

let fields = self.fields(db, specialization, field_policy);
let match_args = fields
.iter()
.filter(|(_, field)| {
if let FieldKind::Dataclass { init, kw_only, .. } = &field.kind {
*init && !kw_only.unwrap_or(kw_only_default)
} else {
false
}
})
.map(|(name, _)| Type::string_literal(db, name));
Some(Type::heterogeneous_tuple(db, match_args))
}
(CodeGeneratorKind::DataclassLike(_), "__weakref__") => {
if !has_dataclass_param(DataclassFlags::WEAKREF_SLOT)
|| !has_dataclass_param(DataclassFlags::SLOTS)
{
return None;
}

// This could probably be `weakref | None`, but it does not seem important enough to
// model it precisely.
Some(UnionType::from_elements(db, [Type::any(), Type::none(db)]))
}
(CodeGeneratorKind::NamedTuple, name) if name != "__init__" => {
KnownClass::NamedTupleFallback
.to_class_literal(db)
Expand Down
Loading