diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 3530ac4da4..e642b25b2e 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -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` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 0940ec1f6d..60cf7c491e 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -24,8 +24,8 @@ use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::{ ApplyTypeMappingVisitor, BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams, DeprecatedInstance, HasRelationToVisitor, KnownInstanceType, - NormalizedVisitor, StringLiteralType, TypeAliasType, TypeMapping, TypeRelation, - TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type, + NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, + TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type, infer_definition_types, todo_type, }; use crate::{ @@ -1862,6 +1862,18 @@ impl<'db> ClassLiteral<'db> { .with_qualifiers(TypeQualifiers::CLASS_VAR); } + if CodeGeneratorKind::NamedTuple.matches(db, self) { + if let Some(field) = self.own_fields(db, specialization).get(name) { + let property_getter_signature = Signature::new( + Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]), + Some(field.declared_ty), + ); + let property_getter = CallableType::single(db, property_getter_signature); + let property = PropertyInstanceType::new(db, Some(property_getter), None); + return Place::bound(Type::PropertyInstance(property)).into(); + } + } + let body_scope = self.body_scope(db); let symbol = class_symbol(db, body_scope, name).map_type(|ty| { // The `__new__` and `__init__` members of a non-specialized generic class are handled