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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens due to literal promotion, I guess.

reveal_type(Member.__init__)

alice = Member(name="Alice", role="admin")
Expand All @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shows that our previous approach, calling default_factory manually, was apparently too naive.

reveal_type(Data.__init__)

data = Data([1, 2, 3])
Expand All @@ -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")
Expand All @@ -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
Expand Down
17 changes: 10 additions & 7 deletions crates/ty_python_semantic/src/types/call/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading