[ty] Use field-specifier return type as the default type for the field (#20915)

## Summary

`dataclasses.field` and field-specifier functions of commonly used
libraries like `pydantic`, `attrs`, and `SQLAlchemy` all return the
default type for the field (or `Any`) instead of an actual `Field`
instance, even if this is not what happens at runtime. Let's make use of
this fact and assume that *all* field specifiers return the type of the
default value of the field.

For standard dataclasses, this leads to more or less the same outcome
(see test diff for details), but this change is important for 3rd party
dataclass-transformers.

## Test Plan

Tested the consequences of this change on the field-specifiers branch as
well.
This commit is contained in:
David Peter 2025-10-16 13:13:45 +02:00 committed by GitHub
parent 0cc663efcd
commit c8133104e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 16 additions and 11 deletions

View file

@ -11,7 +11,7 @@ class Member:
role: str = field(default="user")
tag: str | None = field(default=None, init=False)
# revealed: (self: Member, name: str, role: str = Literal["user"]) -> None
# revealed: (self: Member, name: str, role: str = str) -> None
reveal_type(Member.__init__)
alice = Member(name="Alice", role="admin")
@ -37,7 +37,7 @@ class Data:
content: list[int] = field(default_factory=list)
timestamp: datetime = field(default_factory=datetime.now, init=False)
# revealed: (self: Data, content: list[int] = list[Unknown]) -> None
# revealed: (self: Data, content: list[int] = list[int]) -> None
reveal_type(Data.__init__)
data = Data([1, 2, 3])
@ -63,7 +63,8 @@ class Person:
age: int | None = field(default=None, kw_only=True)
role: str = field(default="user", kw_only=True)
# revealed: (self: Person, name: str, *, age: int | None = None, role: str = Literal["user"]) -> None
# TODO: this would ideally show a default value of `None` for `age`
# revealed: (self: Person, name: str, *, age: int | None = int | None, role: str = str) -> None
reveal_type(Person.__init__)
alice = Person(role="admin", name="Alice")
@ -82,7 +83,8 @@ def get_default() -> str:
reveal_type(field(default=1)) # revealed: dataclasses.Field[Literal[1]]
reveal_type(field(default=None)) # revealed: dataclasses.Field[None]
reveal_type(field(default_factory=get_default)) # revealed: dataclasses.Field[str]
# TODO: this could ideally be `dataclasses.Field[str]` with a better generics solver
reveal_type(field(default_factory=get_default)) # revealed: dataclasses.Field[Unknown]
```
## dataclass_transform field_specifiers

View file

@ -977,13 +977,16 @@ impl<'db> Bindings<'db> {
let kw_only =
overload.parameter_type_by_name("kw_only").unwrap_or(None);
let default_ty = match (default, default_factory) {
(Some(default_ty), _) => Some(default_ty),
(_, Some(default_factory_ty)) => default_factory_ty
.try_call(db, &CallArguments::none())
.ok()
.map(|binding| binding.return_type(db)),
_ => None,
// `dataclasses.field` and field-specifier functions of commonly used
// libraries like `pydantic`, `attrs`, and `SQLAlchemy` all return
// the default type for the field (or `Any`) instead of an actual `Field`
// instance, even if this is not what happens at runtime (see also below).
// We still make use of this fact and pretend that all field specifiers
// return the type of the default value:
let default_ty = if default.is_some() || default_factory.is_some() {
Some(overload.return_ty)
} else {
None
};
let init = init