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
192 changes: 118 additions & 74 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,73 @@ frozen = MyFrozenChildClass()
del frozen.x # TODO this should emit an [invalid-assignment]
```

### frozen/non-frozen inheritance

If a non-frozen dataclass inherits from a frozen dataclass, an exception is raised at runtime. We
catch this error:

<!-- snapshot-diagnostics -->

`a.py`:

```py
from dataclasses import dataclass

@dataclass(frozen=True)
class FrozenBase:
x: int

@dataclass
# error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
class Child(FrozenBase):
y: int
```

Frozen dataclasses inheriting from non-frozen dataclasses are also illegal:

`b.py`:

```py
from dataclasses import dataclass

@dataclass
class Base:
x: int

@dataclass(frozen=True)
# error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
class FrozenChild(Base):
y: int
```

Example of diagnostics when there are multiple files involved:

`module.py`:

```py
import dataclasses

@dataclasses.dataclass(frozen=False)
class NotFrozenBase:
x: int
```

`main.py`:

```py
from functools import total_ordering
from typing import final
from dataclasses import dataclass

from module import NotFrozenBase

@final
@dataclass(frozen=True)
@total_ordering
class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
y: str
```

### `match_args`

If `match_args` is set to `True` (the default), the `__match_args__` attribute is a tuple created
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: dataclasses.md - Dataclasses - Other dataclass parameters - frozen/non-frozen inheritance
mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
---

# Python source files

## a.py

```
1 | from dataclasses import dataclass
2 |
3 | @dataclass(frozen=True)
4 | class FrozenBase:
5 | x: int
6 |
7 | @dataclass
8 | # error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
9 | class Child(FrozenBase):
10 | y: int
```

## b.py

```
1 | from dataclasses import dataclass
2 |
3 | @dataclass
4 | class Base:
5 | x: int
6 |
7 | @dataclass(frozen=True)
8 | # error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
9 | class FrozenChild(Base):
10 | y: int
```

## module.py

```
1 | import dataclasses
2 |
3 | @dataclasses.dataclass(frozen=False)
4 | class NotFrozenBase:
5 | x: int
```

## main.py

```
1 | from functools import total_ordering
2 | from typing import final
3 | from dataclasses import dataclass
4 |
5 | from module import NotFrozenBase
6 |
7 | @final
8 | @dataclass(frozen=True)
9 | @total_ordering
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
11 | y: str
```

# Diagnostics

```
error[invalid-frozen-dataclass-subclass]: Non-frozen dataclass cannot inherit from frozen dataclass
--> src/a.py:7:1
|
5 | x: int
6 |
7 | @dataclass
| ---------- `Child` dataclass parameters
8 | # error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
9 | class Child(FrozenBase):
| ^^^^^^----------^ Subclass `Child` is not frozen but base class `FrozenBase` is
10 | y: int
|
info: This causes the class creation to fail
info: Base class definition
--> src/a.py:3:1
|
1 | from dataclasses import dataclass
2 |
3 | @dataclass(frozen=True)
| ----------------------- `FrozenBase` dataclass parameters
4 | class FrozenBase:
| ^^^^^^^^^^ `FrozenBase` definition
5 | x: int
|
info: rule `invalid-frozen-dataclass-subclass` is enabled by default

```

```
error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass
--> src/b.py:7:1
|
5 | x: int
6 |
7 | @dataclass(frozen=True)
| ----------------------- `FrozenChild` dataclass parameters
8 | # error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
9 | class FrozenChild(Base):
| ^^^^^^^^^^^^----^ Subclass `FrozenChild` is frozen but base class `Base` is not
10 | y: int
|
info: This causes the class creation to fail
info: Base class definition
--> src/b.py:3:1
|
1 | from dataclasses import dataclass
2 |
3 | @dataclass
| ---------- `Base` dataclass parameters
4 | class Base:
| ^^^^ `Base` definition
5 | x: int
|
info: rule `invalid-frozen-dataclass-subclass` is enabled by default

```

```
error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass
--> src/main.py:8:1
|
7 | @final
8 | @dataclass(frozen=True)
| ----------------------- `FrozenChild` dataclass parameters
9 | @total_ordering
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
| ^^^^^^^^^^^^-------------^ Subclass `FrozenChild` is frozen but base class `NotFrozenBase` is not
11 | y: str
|
info: This causes the class creation to fail
info: Base class definition
--> src/module.py:3:1
|
1 | import dataclasses
2 |
3 | @dataclasses.dataclass(frozen=False)
| ------------------------------------ `NotFrozenBase` dataclass parameters
4 | class NotFrozenBase:
| ^^^^^^^^^^^^^ `NotFrozenBase` definition
5 | x: int
|
info: rule `invalid-frozen-dataclass-subclass` is enabled by default

```
6 changes: 6 additions & 0 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,12 @@ bitflags! {
}
}

impl DataclassFlags {
pub(crate) const fn is_frozen(self) -> bool {
self.contains(Self::FROZEN)
}
}

pub(crate) const DATACLASS_FLAGS: &[(&str, DataclassFlags)] = &[
("init", DataclassFlags::INIT),
("repr", DataclassFlags::REPR),
Expand Down
22 changes: 22 additions & 0 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1856,6 +1856,28 @@ impl<'db> ClassLiteral<'db> {
.filter_map(|decorator| decorator.known(db))
}

/// Iterate through the decorators on this class, returning the position of the first one
/// that matches the given predicate.
pub(super) fn find_decorator_position(
self,
db: &'db dyn Db,
predicate: impl Fn(Type<'db>) -> bool,
) -> Option<usize> {
self.decorators(db)
.iter()
.position(|decorator| predicate(*decorator))
}

/// Iterate through the decorators on this class, returning the index of the first one
/// that is either `@dataclass` or `@dataclass(...)`.
pub(super) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option<usize> {
self.find_decorator_position(db, |ty| match ty {
Type::FunctionLiteral(function) => function.is_known(db, KnownFunction::Dataclass),
Type::DataclassDecorator(_) => true,
_ => false,
})
}

/// Is this class final?
pub(super) fn is_final(self, db: &'db dyn Db) -> bool {
self.known_function_decorators(db)
Expand Down
Loading