Skip to content

Commit a5e16e4

Browse files
committed
[ty] Synthesize read-only properties for all declared members on NamedTuple classes
1 parent 9f6146a commit a5e16e4

File tree

2 files changed

+58
-5
lines changed

2 files changed

+58
-5
lines changed

crates/ty_python_semantic/resources/mdtest/named_tuple.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,16 @@ Person(3, "Eve", 99, "extra")
7474
# error: [invalid-argument-type]
7575
Person(id="3", name="Eve")
7676

77-
# TODO: over-writing NamedTuple fields should be an error
77+
reveal_type(Person.id) # revealed: property
78+
reveal_type(Person.name) # revealed: property
79+
reveal_type(Person.age) # revealed: property
80+
81+
# TODO... the error is correct, but this is not the friendliest error message
82+
# for assigning to a read-only property :-)
83+
#
84+
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `id` on type `Person` with custom `__set__` method"
7885
alice.id = 42
86+
# error: [invalid-assignment]
7987
bob.age = None
8088
```
8189

@@ -151,9 +159,42 @@ from typing import NamedTuple
151159
class User(NamedTuple):
152160
id: int
153161
name: str
162+
age: int | None
163+
nickname: str
154164

155165
class SuperUser(User):
156-
id: int # this should be an error
166+
# this should be an error because it implies that the `id` attribute on
167+
# `SuperUser` is mutable, but the read-only `id` property from the superclass
168+
# has not been overridden in the class body
169+
id: int
170+
171+
# this is fine; overriding a read-only attribute with a mutable one
172+
# does not conflict with the Liskov Substitution Principle
173+
name: str = "foo"
174+
175+
# this is also fine
176+
@property
177+
def age(self) -> int:
178+
return super().age or 42
179+
180+
def now_called_robert(self):
181+
self.name = "Robert" # fine because overridden with a mutable attribute
182+
183+
# TODO: this should cause us to emit an error as we're assigning to a read-only property
184+
# inherited from the `NamedTuple` superclass (requires https://github.com/astral-sh/ty/issues/159)
185+
self.nickname = "Bob"
186+
187+
james = SuperUser(0, "James", 42, "Jimmy")
188+
189+
# fine because the property on the superclass was overridden with a mutable attribute
190+
# on the subclass
191+
james.name = "Robert"
192+
193+
# TODO: the error is correct (can't assign to the read-only property inherited from the superclass)
194+
# but the error message could be friendlier :-)
195+
#
196+
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `nickname` on type `SuperUser` with custom `__set__` method"
197+
james.nickname = "Bob"
157198
```
158199

159200
### Generic named tuples

crates/ty_python_semantic/src/types/class.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signatu
2323
use crate::types::tuple::{TupleSpec, TupleType};
2424
use crate::types::{
2525
BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams,
26-
DeprecatedInstance, KnownInstanceType, StringLiteralType, TypeAliasType, TypeMapping,
27-
TypeRelation, TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
28-
declaration_type, infer_definition_types, todo_type,
26+
DeprecatedInstance, KnownInstanceType, PropertyInstanceType, StringLiteralType, TypeAliasType,
27+
TypeMapping, TypeRelation, TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance,
28+
TypeVarKind, declaration_type, infer_definition_types, todo_type,
2929
};
3030
use crate::{
3131
Db, FxIndexMap, FxOrderSet, Program,
@@ -1831,6 +1831,18 @@ impl<'db> ClassLiteral<'db> {
18311831
.with_qualifiers(TypeQualifiers::CLASS_VAR);
18321832
}
18331833

1834+
if CodeGeneratorKind::NamedTuple.matches(db, self) {
1835+
if let Some(field) = self.own_fields(db, specialization).get(name) {
1836+
let property_getter_signature = Signature::new(
1837+
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]),
1838+
Some(field.declared_ty),
1839+
);
1840+
let property_getter = CallableType::single(db, property_getter_signature);
1841+
let property = PropertyInstanceType::new(db, Some(property_getter), None);
1842+
return Place::bound(Type::PropertyInstance(property)).into();
1843+
}
1844+
}
1845+
18341846
let body_scope = self.body_scope(db);
18351847
let symbol = class_symbol(db, body_scope, name).map_type(|ty| {
18361848
// The `__new__` and `__init__` members of a non-specialized generic class are handled

0 commit comments

Comments
 (0)