mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-20 04:29:47 +00:00
[ty] Support dataclass-transform field_specifiers (#20888)
## Summary
Add support for the `field_specifiers` parameter on
`dataclass_transform` decorator calls.
closes https://github.com/astral-sh/ty/issues/1068
## Conformance test results
All true positives ✔️
## Ecosystem analysis
* `trio`: this is the kind of change that I would expect from this PR.
The code makes use of a dataclass `Outcome` with a `_unwrapped: bool =
attr.ib(default=False, eq=False, init=False)` field that is excluded
from the `__init__` signature, so we now see a bunch of
constructor-call-related errors going away.
* `home-assistant/core`: They have a `domain: str = attr.ib(init=False,
repr=False)` field and then use
```py
@domain.default
def _domain_default(self) -> str:
# …
```
This accesses the `default` attribute on `dataclasses.Field[…]` with a
type of `default: _T | Literal[_MISSING_TYPE.MISSING]`, so we get those
"Object of type `_MISSING_TYPE` is not callable" errors. I don't really
understand how that is supposed to work. Even if `_MISSING_TYPE` would
be absent from that union, what does this try to call? pyright also
issues an error and it doesn't seem to work at runtime? So this looks
like a true positive?
* `attrs`: Similar here. There are some new diagnostics on code that
tries to access `.validator` on a field. This *does* work at runtime,
but I'm not sure how that is supposed to type-check (without a [custom
plugin](2c6c395935/mypy/plugins/attrs.py (L575-L602))).
pyright errors on this as well.
* A handful of new false positives because we don't support `alias` yet
## Test Plan
Updated tests.
This commit is contained in:
parent
2bffef5966
commit
8dad58de37
7 changed files with 475 additions and 198 deletions
|
|
@ -461,7 +461,7 @@ The [`typing.dataclass_transform`] specification also allows classes (such as `d
|
|||
to be listed in `field_specifiers`, but it is currently unclear how this should work, and other type
|
||||
checkers do not seem to support this either.
|
||||
|
||||
### Basic example
|
||||
### For function-based transformers
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, Any
|
||||
|
|
@ -478,11 +478,8 @@ class Person:
|
|||
name: str = fancy_field()
|
||||
age: int | None = fancy_field(kw_only=True)
|
||||
|
||||
# TODO: Should be `(self: Person, name: str, *, age: int | None) -> None`
|
||||
reveal_type(Person.__init__) # revealed: (self: Person, id: int = Any, name: str = Any, age: int | None = Any) -> None
|
||||
reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None
|
||||
|
||||
# TODO: No error here
|
||||
# error: [invalid-argument-type]
|
||||
alice = Person("Alice", age=30)
|
||||
|
||||
reveal_type(alice.id) # revealed: int
|
||||
|
|
@ -490,6 +487,145 @@ reveal_type(alice.name) # revealed: str
|
|||
reveal_type(alice.age) # revealed: int | None
|
||||
```
|
||||
|
||||
### For metaclass-based transformers
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, Any
|
||||
|
||||
def fancy_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(fancy_field,))
|
||||
class FancyMeta(type):
|
||||
def __new__(cls, name, bases, namespace):
|
||||
...
|
||||
return super().__new__(cls, name, bases, namespace)
|
||||
|
||||
class FancyBase(metaclass=FancyMeta): ...
|
||||
|
||||
class Person(FancyBase):
|
||||
id: int = fancy_field(init=False)
|
||||
name: str = fancy_field()
|
||||
age: int | None = fancy_field(kw_only=True)
|
||||
|
||||
reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None
|
||||
|
||||
alice = Person("Alice", age=30)
|
||||
|
||||
reveal_type(alice.id) # revealed: int
|
||||
reveal_type(alice.name) # revealed: str
|
||||
reveal_type(alice.age) # revealed: int | None
|
||||
```
|
||||
|
||||
### For base-class-based transformers
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, Any
|
||||
|
||||
def fancy_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(fancy_field,))
|
||||
class FancyBase:
|
||||
def __init_subclass__(cls):
|
||||
...
|
||||
super().__init_subclass__()
|
||||
|
||||
class Person(FancyBase):
|
||||
id: int = fancy_field(init=False)
|
||||
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
|
||||
|
||||
# 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
|
||||
reveal_type(alice.name) # revealed: str
|
||||
reveal_type(alice.age) # revealed: int | None
|
||||
```
|
||||
|
||||
### With default arguments
|
||||
|
||||
Field specifiers can have default arguments that should be respected:
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, Any
|
||||
|
||||
def fancy_field(*, init: bool = False) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(fancy_field,))
|
||||
def fancy_model[T](cls: type[T]) -> type[T]:
|
||||
...
|
||||
return cls
|
||||
|
||||
@fancy_model
|
||||
class Person:
|
||||
id: int = fancy_field()
|
||||
name: str = fancy_field(init=True)
|
||||
|
||||
reveal_type(Person.__init__) # revealed: (self: Person, name: str) -> None
|
||||
|
||||
Person(name="Alice")
|
||||
```
|
||||
|
||||
### With overloaded field specifiers
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, overload, Any
|
||||
|
||||
@overload
|
||||
def fancy_field(*, init: bool = True) -> Any: ...
|
||||
@overload
|
||||
def fancy_field(*, kw_only: bool = False) -> Any: ...
|
||||
def fancy_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(fancy_field,))
|
||||
def fancy_model[T](cls: type[T]) -> type[T]:
|
||||
...
|
||||
return cls
|
||||
|
||||
@fancy_model
|
||||
class Person:
|
||||
id: int = fancy_field(init=False)
|
||||
name: str = fancy_field()
|
||||
age: int | None = fancy_field(kw_only=True)
|
||||
|
||||
reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None
|
||||
```
|
||||
|
||||
### Nested dataclass-transformers
|
||||
|
||||
Make sure that models are only affected by the field specifiers of their own transformer:
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, Any
|
||||
from dataclasses import field
|
||||
|
||||
def outer_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(outer_field,))
|
||||
def outer_model[T](cls: type[T]) -> type[T]:
|
||||
# ...
|
||||
return cls
|
||||
|
||||
def inner_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(inner_field,))
|
||||
def inner_model[T](cls: type[T]) -> type[T]:
|
||||
# ...
|
||||
return cls
|
||||
|
||||
@outer_model
|
||||
class Outer:
|
||||
@inner_model
|
||||
class Inner:
|
||||
inner_a: int = inner_field(init=False)
|
||||
inner_b: str = outer_field(init=False)
|
||||
|
||||
outer_a: int = outer_field(init=False)
|
||||
outer_b: str = inner_field(init=False)
|
||||
|
||||
reveal_type(Outer.__init__) # revealed: (self: Outer, outer_b: str = Any) -> None
|
||||
reveal_type(Outer.Inner.__init__) # revealed: (self: Inner, inner_b: str = Any) -> None
|
||||
```
|
||||
|
||||
## Overloaded dataclass-like decorators
|
||||
|
||||
In the case of an overloaded decorator, the `dataclass_transform` decorator can be applied to the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue