[ty] Synthesize read-only properties for all declared members on NamedTuple classes (#19899)

This commit is contained in:
Alex Waygood 2025-08-14 22:25:45 +01:00 committed by GitHub
parent 82350a398e
commit f6093452ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 74 additions and 5 deletions

View file

@ -74,8 +74,16 @@ Person(3, "Eve", 99, "extra")
# error: [invalid-argument-type]
Person(id="3", name="Eve")
# TODO: over-writing NamedTuple fields should be an error
reveal_type(Person.id) # revealed: property
reveal_type(Person.name) # revealed: property
reveal_type(Person.age) # revealed: property
# TODO... the error is correct, but this is not the friendliest error message
# for assigning to a read-only property :-)
#
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `id` on type `Person` with custom `__set__` method"
alice.id = 42
# error: [invalid-assignment]
bob.age = None
```
@ -151,9 +159,42 @@ from typing import NamedTuple
class User(NamedTuple):
id: int
name: str
age: int | None
nickname: str
class SuperUser(User):
id: int # this should be an error
# TODO: this should be an error because it implies that the `id` attribute on
# `SuperUser` is mutable, but the read-only `id` property from the superclass
# has not been overridden in the class body
id: int
# this is fine; overriding a read-only attribute with a mutable one
# does not conflict with the Liskov Substitution Principle
name: str = "foo"
# this is also fine
@property
def age(self) -> int:
return super().age or 42
def now_called_robert(self):
self.name = "Robert" # fine because overridden with a mutable attribute
# TODO: this should cause us to emit an error as we're assigning to a read-only property
# inherited from the `NamedTuple` superclass (requires https://github.com/astral-sh/ty/issues/159)
self.nickname = "Bob"
james = SuperUser(0, "James", 42, "Jimmy")
# fine because the property on the superclass was overridden with a mutable attribute
# on the subclass
james.name = "Robert"
# TODO: the error is correct (can't assign to the read-only property inherited from the superclass)
# but the error message could be friendlier :-)
#
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `nickname` on type `SuperUser` with custom `__set__` method"
james.nickname = "Bob"
```
### Generic named tuples
@ -164,13 +205,29 @@ python-version = "3.12"
```
```py
from typing import NamedTuple
from typing import NamedTuple, Generic, TypeVar
class Property[T](NamedTuple):
name: str
value: T
reveal_type(Property("height", 3.4)) # revealed: Property[float]
reveal_type(Property.value) # revealed: property
reveal_type(Property.value.fget) # revealed: (self, /) -> Unknown
reveal_type(Property[str].value.fget) # revealed: (self, /) -> str
reveal_type(Property("height", 3.4).value) # revealed: float
T = TypeVar("T")
class LegacyProperty(NamedTuple, Generic[T]):
name: str
value: T
reveal_type(LegacyProperty("height", 42)) # revealed: LegacyProperty[int]
reveal_type(LegacyProperty.value) # revealed: property
reveal_type(LegacyProperty.value.fget) # revealed: (self, /) -> Unknown
reveal_type(LegacyProperty[str].value.fget) # revealed: (self, /) -> str
reveal_type(LegacyProperty("height", 3.4).value) # revealed: float
```
## Attributes on `NamedTuple`