[ty] NamedTuple 'fallback' attributes (#18127)

## Summary

Add various attributes to `NamedTuple` classes/instances that are
available at runtime.

closes https://github.com/astral-sh/ty/issues/417

## Test Plan

New Markdown tests
This commit is contained in:
David Peter 2025-05-16 12:56:43 +02:00 committed by GitHub
parent 8644c9da43
commit e67b35743a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 53 additions and 5 deletions

View file

@ -139,6 +139,33 @@ class Property[T](NamedTuple):
reveal_type(Property("height", 3.4)) # revealed: Property[Unknown] reveal_type(Property("height", 3.4)) # revealed: Property[Unknown]
``` ```
## Attributes on `NamedTuple`
The following attributes are available on `NamedTuple` classes / instances:
```py
from typing import NamedTuple
class Person(NamedTuple):
name: str
age: int | None = None
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
reveal_type(Person._fields) # revealed: tuple[str, ...]
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Self
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self
# TODO: should be `Person` once we support `Self`
reveal_type(Person._make(("Alice", 42))) # revealed: Unknown
person = Person("Alice", 42)
reveal_type(person._asdict()) # revealed: dict[str, Any]
# TODO: should be `Person` once we support `Self`
reveal_type(person._replace(name="Bob")) # revealed: Unknown
```
## `collections.namedtuple` ## `collections.namedtuple`
```py ```py

View file

@ -118,6 +118,8 @@ pub enum KnownModule {
Dataclasses, Dataclasses,
Collections, Collections,
Inspect, Inspect,
#[strum(serialize = "_typeshed._type_checker_internals")]
TypeCheckerInternals,
TyExtensions, TyExtensions,
} }
@ -135,6 +137,7 @@ impl KnownModule {
Self::Dataclasses => "dataclasses", Self::Dataclasses => "dataclasses",
Self::Collections => "collections", Self::Collections => "collections",
Self::Inspect => "inspect", Self::Inspect => "inspect",
Self::TypeCheckerInternals => "_typeshed._type_checker_internals",
Self::TyExtensions => "ty_extensions", Self::TyExtensions => "ty_extensions",
} }
} }

View file

@ -1289,6 +1289,14 @@ impl<'db> ClassLiteral<'db> {
Some(Type::Callable(CallableType::single(db, signature))) Some(Type::Callable(CallableType::single(db, signature)))
} }
(CodeGeneratorKind::NamedTuple, name) if name != "__init__" => {
KnownClass::NamedTupleFallback
.to_class_literal(db)
.into_class_literal()?
.own_class_member(db, None, name)
.symbol
.ignore_possibly_unbound()
}
_ => None, _ => None,
} }
} }
@ -1985,6 +1993,8 @@ pub enum KnownClass {
NotImplementedType, NotImplementedType,
// dataclasses // dataclasses
Field, Field,
// _typeshed._type_checker_internals
NamedTupleFallback,
} }
impl<'db> KnownClass { impl<'db> KnownClass {
@ -2067,7 +2077,8 @@ impl<'db> KnownClass {
// (see https://docs.python.org/3/library/constants.html#NotImplemented) // (see https://docs.python.org/3/library/constants.html#NotImplemented)
| Self::NotImplementedType | Self::NotImplementedType
| Self::Classmethod | Self::Classmethod
| Self::Field => Truthiness::Ambiguous, | Self::Field
| Self::NamedTupleFallback => Truthiness::Ambiguous,
} }
} }
@ -2141,7 +2152,8 @@ impl<'db> KnownClass {
| Self::EllipsisType | Self::EllipsisType
| Self::NotImplementedType | Self::NotImplementedType
| Self::UnionType | Self::UnionType
| Self::Field => false, | Self::Field
| Self::NamedTupleFallback => false,
} }
} }
@ -2217,6 +2229,7 @@ impl<'db> KnownClass {
} }
Self::NotImplementedType => "_NotImplementedType", Self::NotImplementedType => "_NotImplementedType",
Self::Field => "Field", Self::Field => "Field",
Self::NamedTupleFallback => "NamedTupleFallback",
} }
} }
@ -2446,6 +2459,7 @@ impl<'db> KnownClass {
| Self::Deque | Self::Deque
| Self::OrderedDict => KnownModule::Collections, | Self::OrderedDict => KnownModule::Collections,
Self::Field => KnownModule::Dataclasses, Self::Field => KnownModule::Dataclasses,
Self::NamedTupleFallback => KnownModule::TypeCheckerInternals,
} }
} }
@ -2508,7 +2522,8 @@ impl<'db> KnownClass {
| Self::Super | Self::Super
| Self::NamedTuple | Self::NamedTuple
| Self::NewType | Self::NewType
| Self::Field => false, | Self::Field
| Self::NamedTupleFallback => false,
} }
} }
@ -2573,7 +2588,8 @@ impl<'db> KnownClass {
| Self::UnionType | Self::UnionType
| Self::NamedTuple | Self::NamedTuple
| Self::NewType | Self::NewType
| Self::Field => false, | Self::Field
| Self::NamedTupleFallback => false,
} }
} }
@ -2646,6 +2662,7 @@ impl<'db> KnownClass {
} }
"_NotImplementedType" => Self::NotImplementedType, "_NotImplementedType" => Self::NotImplementedType,
"Field" => Self::Field, "Field" => Self::Field,
"NamedTupleFallback" => Self::NamedTupleFallback,
_ => return None, _ => return None,
}; };
@ -2700,7 +2717,8 @@ impl<'db> KnownClass {
| Self::GeneratorType | Self::GeneratorType
| Self::AsyncGeneratorType | Self::AsyncGeneratorType
| Self::WrapperDescriptorType | Self::WrapperDescriptorType
| Self::Field => module == self.canonical_module(db), | Self::Field
| Self::NamedTupleFallback => module == self.canonical_module(db),
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
Self::SpecialForm Self::SpecialForm
| Self::TypeVar | Self::TypeVar