[ty] Synthesize a _fields attribute for NamedTuples (#22163)

## Summary

Closes #2176.
This commit is contained in:
Charlie Marsh
2025-12-23 16:38:41 -05:00
committed by GitHub
parent 89a55dd09f
commit 5decf94644
3 changed files with 15 additions and 3 deletions

View File

@@ -266,7 +266,7 @@ class Person(NamedTuple):
age: int | None = None
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
reveal_type(Person._fields) # revealed: tuple[str, ...]
reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]]
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Person
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Person._replace) # revealed: (self: Self, *, name: str = ..., age: int | None = ...) -> Self

View File

@@ -2583,6 +2583,14 @@ impl<'db> ClassLiteral<'db> {
.with_annotated_type(self_ty);
signature_from_fields(vec![self_parameter], Some(self_ty))
}
(CodeGeneratorKind::NamedTuple, "_fields") => {
// Synthesize a precise tuple type for _fields using literal string types.
// For example, a NamedTuple with `name` and `age` fields gets
// `tuple[Literal["name"], Literal["age"]]`.
let fields = self.fields(db, specialization, field_policy);
let field_types = fields.keys().map(|name| Type::string_literal(db, name));
Some(Type::heterogeneous_tuple(db, field_types))
}
(CodeGeneratorKind::DataclassLike(_), "__lt__" | "__le__" | "__gt__" | "__ge__") => {
if !has_dataclass_param(DataclassFlags::ORDER) {
return None;

View File

@@ -184,9 +184,13 @@ def reveal_mro(cls: type | types.GenericAlias) -> None:
# A protocol describing an interface that should be satisfied by all named tuples
# created using `typing.NamedTuple` or `collections.namedtuple`.
class NamedTupleLike(Protocol):
# from typing.NamedTuple stub
# _fields is defined as `tuple[Any, ...]` rather than `tuple[str, ...]` so
# that instances of actual `NamedTuple` classes with more precise `_fields`
# types are considered assignable to this protocol (protocol attribute members
# are invariant, and `tuple[str, str]` is not invariantly assignable to
# `tuple[str, ...]`).
_fields: ClassVar[tuple[Any, ...]]
_field_defaults: ClassVar[dict[str, Any]]
_fields: ClassVar[tuple[str, ...]]
@classmethod
def _make(cls: type[Self], iterable: Iterable[Any]) -> Self: ...
def _asdict(self, /) -> dict[str, Any]: ...