[ty] Support dataclass_transform for base class models (#20783)

## Summary

Support `dataclass_transform` when used on a (base) class.

## Typing conformance

* The changes in `dataclasses_transform_class.py` look good, just a few
mistakes due to missing `alias` support.
* I didn't look closely at the changes in
`dataclasses_transform_converter.py` since we don't support `converter`
yet.

## Ecosystem impact

The impact looks huge, but it's concentrated on a single project (ibis).
Their setup looks more or less like this:

* the real `Annotatable`:
d7083c2c96/ibis/common/grounds.py (L100-L101)
* the real `DataType`:
d7083c2c96/ibis/expr/datatypes/core.py (L161-L179)
* the real `Array`:
d7083c2c96/ibis/expr/datatypes/core.py (L1003-L1006)


```py
from typing import dataclass_transform

@dataclass_transform()
class Annotatable:
    pass

class DataType(Annotatable):
    nullable: bool = True

class Array[T](DataType):
    value_type: T
```

They expect something like `Array([1, 2])` to work, but ty, pyright,
mypy, and pyrefly would all expect there to be a first argument for the
`nullable` field on `DataType`. I don't really understand on what
grounds they expect the `nullable` field to be excluded from the
signature, but this seems to be the main reason for the new diagnostics
here. Not sure if related, but it looks like their typing setup is not
really complete
(https://github.com/ibis-project/ibis/issues/6844#issuecomment-1868274770,
this thread also mentions `dataclass_transform`).

## Test Plan

Update pre-existing tests.
This commit is contained in:
David Peter 2025-10-17 14:04:31 +02:00 committed by GitHub
parent fc3b341529
commit cfbd42c22a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 56 additions and 45 deletions

View file

@ -122,9 +122,6 @@ class CustomerModel(ModelBase):
id: int
name: str
# TODO: this is not supported yet
# error: [unknown-argument]
# error: [unknown-argument]
CustomerModel(id=1, name="Test")
```
@ -216,11 +213,7 @@ class OrderedModelBase: ...
class TestWithBase(OrderedModelBase):
inner: int
# TODO: No errors here, should reveal `bool`
# error: [too-many-positional-arguments]
# error: [too-many-positional-arguments]
# error: [unsupported-operator]
reveal_type(TestWithBase(1) < TestWithBase(2)) # revealed: Unknown
reveal_type(TestWithBase(1) < TestWithBase(2)) # revealed: bool
```
### `kw_only_default`
@ -277,8 +270,7 @@ class ModelBase: ...
class TestBase(ModelBase):
name: str
# TODO: This should be `(self: TestBase, *, name: str) -> None`
reveal_type(TestBase.__init__) # revealed: def __init__(self) -> None
reveal_type(TestBase.__init__) # revealed: (self: TestBase, *, name: str) -> None
```
### `frozen_default`
@ -333,12 +325,9 @@ class ModelBase: ...
class TestMeta(ModelBase):
name: str
# TODO: no error here
# error: [unknown-argument]
t = TestMeta(name="test")
# TODO: this should be an `invalid-assignment` error
t.name = "new"
t.name = "new" # error: [invalid-assignment]
```
### Combining parameters
@ -437,19 +426,15 @@ class DefaultFrozenModel:
class Frozen(DefaultFrozenModel):
name: str
# TODO: no error here
# error: [unknown-argument]
f = Frozen(name="test")
# TODO: this should be an `invalid-assignment` error
f.name = "new"
f.name = "new" # error: [invalid-assignment]
class Mutable(DefaultFrozenModel, frozen=False):
name: str
# TODO: no error here
# error: [unknown-argument]
m = Mutable(name="test")
m.name = "new" # No error
# TODO: This should not be an error
m.name = "new" # error: [invalid-assignment]
```
## `field_specifiers`
@ -532,12 +517,8 @@ class Person(FancyBase):
name: str = fancy_field()
age: int | None = fancy_field(kw_only=True)
# TODO: should be (self: Person, name: str = Unknown, *, age: int | None = Unknown) -> None
reveal_type(Person.__init__) # revealed: def __init__(self) -> None
reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None
# TODO: shouldn't be an error
# error: [too-many-positional-arguments]
# error: [unknown-argument]
alice = Person("Alice", age=30)
reveal_type(alice.id) # revealed: int