mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-21 15:52:34 +00:00
[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:
parent
0cc663efcd
commit
c8133104e8
2 changed files with 16 additions and 11 deletions
|
@ -11,7 +11,7 @@ class Member:
|
||||||
role: str = field(default="user")
|
role: str = field(default="user")
|
||||||
tag: str | None = field(default=None, init=False)
|
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__)
|
reveal_type(Member.__init__)
|
||||||
|
|
||||||
alice = Member(name="Alice", role="admin")
|
alice = Member(name="Alice", role="admin")
|
||||||
|
@ -37,7 +37,7 @@ class Data:
|
||||||
content: list[int] = field(default_factory=list)
|
content: list[int] = field(default_factory=list)
|
||||||
timestamp: datetime = field(default_factory=datetime.now, init=False)
|
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__)
|
reveal_type(Data.__init__)
|
||||||
|
|
||||||
data = Data([1, 2, 3])
|
data = Data([1, 2, 3])
|
||||||
|
@ -63,7 +63,8 @@ class Person:
|
||||||
age: int | None = field(default=None, kw_only=True)
|
age: int | None = field(default=None, kw_only=True)
|
||||||
role: str = field(default="user", 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__)
|
reveal_type(Person.__init__)
|
||||||
|
|
||||||
alice = Person(role="admin", name="Alice")
|
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=1)) # revealed: dataclasses.Field[Literal[1]]
|
||||||
reveal_type(field(default=None)) # revealed: dataclasses.Field[None]
|
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
|
## dataclass_transform field_specifiers
|
||||||
|
|
|
@ -977,13 +977,16 @@ impl<'db> Bindings<'db> {
|
||||||
let kw_only =
|
let kw_only =
|
||||||
overload.parameter_type_by_name("kw_only").unwrap_or(None);
|
overload.parameter_type_by_name("kw_only").unwrap_or(None);
|
||||||
|
|
||||||
let default_ty = match (default, default_factory) {
|
// `dataclasses.field` and field-specifier functions of commonly used
|
||||||
(Some(default_ty), _) => Some(default_ty),
|
// libraries like `pydantic`, `attrs`, and `SQLAlchemy` all return
|
||||||
(_, Some(default_factory_ty)) => default_factory_ty
|
// the default type for the field (or `Any`) instead of an actual `Field`
|
||||||
.try_call(db, &CallArguments::none())
|
// instance, even if this is not what happens at runtime (see also below).
|
||||||
.ok()
|
// We still make use of this fact and pretend that all field specifiers
|
||||||
.map(|binding| binding.return_type(db)),
|
// return the type of the default value:
|
||||||
_ => None,
|
let default_ty = if default.is_some() || default_factory.is_some() {
|
||||||
|
Some(overload.return_ty)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let init = init
|
let init = init
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue